Ashlar.Mcp.Server.Host
0.4.2
{ "inputs": [ { "type": "promptString", "id": "Ashlar__Mcp__Bricks__ExposedBrickIds__0", "description": "Brick to expose, e.g. github.blockers. Nothing is exposed until at least one id is listed." }, { "type": "promptString", "id": "Ashlar__Mcp__Bricks__ExposedBrickIds__1", "description": "Optional second brick, e.g. github.pr-status or github.ci-status." } ], "servers": { "Ashlar.Mcp.Server.Host": { "type": "stdio", "command": "dnx", "args": ["Ashlar.Mcp.Server.Host@0.4.2", "--yes"], "env": { "Ashlar__Mcp__Bricks__ExposedBrickIds__0": "${input:Ashlar__Mcp__Bricks__ExposedBrickIds__0}", "Ashlar__Mcp__Bricks__ExposedBrickIds__1": "${input:Ashlar__Mcp__Bricks__ExposedBrickIds__1}" } } } }
.vscode/mcp.json settings file.
dotnet tool install --global Ashlar.Mcp.Server.Host --version 0.4.2
dotnet new tool-manifest
dotnet tool install --local Ashlar.Mcp.Server.Host --version 0.4.2
#tool dotnet:?package=Ashlar.Mcp.Server.Host&version=0.4.2
nuke :add-package Ashlar.Mcp.Server.Host --version 0.4.2
Ashlar.Mcp.Server.Host
An MCP server over stdio that exposes Ashlar bricks (typed, deterministic .NET units of work) and read-only repository tools to any MCP client: VS Code / GitHub Copilot, Claude Code, Cursor, Visual Studio. Every brick call writes one audit record you can match to the exact request and response.
Nothing is exposed by default. You list the bricks (and tools) a client may call.
Run it
Needs the .NET 10 SDK, which ships dnx.
Ashlar__Mcp__Bricks__ExposedBrickIds__0=github.blockers dnx Ashlar.Mcp.Server.Host --yes
VS Code (.vscode/mcp.json):
{
"servers": {
"ashlar": {
"type": "stdio",
"command": "dnx",
"args": ["Ashlar.Mcp.Server.Host", "--yes"],
"env": {
"Ashlar__Mcp__Bricks__ExposedBrickIds__0": "github.blockers",
"Ashlar__Mcp__Bricks__ExposedBrickIds__1": "github.ci-status"
}
}
}
}
Pin a version with Ashlar.Mcp.Server.Host@<version> in args.
From a clone
The package has been on nuget.org since 0.4.0. To run unreleased changes, or a version older than 0.4.0 (where
dnx answers ashlar.mcp.server.host is not found in NuGet feeds), run the server from a clone:
dotnet build src/Ashlar.Mcp.Server.Host -c Release
Ashlar__Mcp__Bricks__ExposedBrickIds__0=github.blockers \
dotnet src/Ashlar.Mcp.Server.Host/bin/Release/net10.0/Ashlar.Mcp.Server.Host.dll
In an MCP client configuration, set "command": "dotnet" and pass the .dll path as the only args entry.
Build once and start the built .dll, so a client does not wait for a build on every start.
Built-in bricks
| Brick id | MCP tool name | What it does |
|---|---|---|
github.blockers |
github_blockers |
What is blocking a pull request or release: a verdict, each blocker with an evidence link, and every fact it could not read (with the reason). Input: url (https://github.com/<owner>/<repo>/pull/<n> or .../releases/tag/<tag>); optional githubToken. |
github.pr-status |
github_pr-status |
Pull request state, mergeability, reviews, checks and the blockers they add up to. Input: prUrl; optional githubToken. |
github.ci-status |
github_ci-status |
Workflow runs and check runs for a ref. Inputs: owner, repo, ref; optional githubToken. |
All three call api.github.com and accept only https://github.com URLs. Without a token GitHub allows 60 requests
an hour; pass githubToken for more, and to read private repositories.
What a call returns
A successful call returns a JSON text block (also sent as structured content):
{"brickId":"github.pr-status","brickVersion":"1.0.0","auditId":"3f2c…","summary":"…","outputs":{ … }}
Missing required inputs and arguments that are not a JSON object come back as tool errors (isError: true)
before the brick runs, and also carry an auditId. A brick that throws comes back the same way. A tool name the
server does not list (including a registered brick that is not allowlisted) is an MCP protocol error, not a tool
result; it is recorded too, and the error message carries its auditId. A tools/call whose arguments is not a
JSON object, or a frame that is not valid JSON-RPC, is rejected by the MCP SDK before any Ashlar code runs, and is
not recorded.
Audit records
One JSON line per brick call (refusals included) in
<LocalApplicationData>/ashlar/mcp-audit/mcp-brick-calls-<start time>.ndjson (on Linux
~/.local/share/ashlar/mcp-audit), one file per server start, none deleted by the server. Set
Ashlar__Audit__File__Directory to move it.
To find the record for one call: grep <auditId> ~/.local/share/ashlar/mcp-audit/mcp-brick-calls-*.ndjson. Each record has eventType MCP_BRICK_INVOKED,
correlationId = the auditId the caller saw, and a detail holding the brick id and version, the outcome
(succeeded, failed, refused), the duration, and the SHA-256 of the arguments as received and of the text
returned. A refused unknown tool name has agentName mcp.unknown-tool, a null brickId, and the name as
requestedToolSha256 plus requestedTool (cut to 64 characters, anything outside [A-Za-z0-9_.-] replaced by _). Argument and output values are not written, so a token passed as githubToken does not land on disk.
inputSha256 is the hash of the arguments as the server re-serializes them: compact JSON, properties in the
order sent, strings escaped the System.Text.Json default way (non-ASCII and <>&'+ as \uXXXX), not the bytes
the client sent. resultSha256 is the hash of the exact text returned. These are plain SHA-256, not keyed: an
argument with few possible values (a public pull request URL) can be recovered from its hash by trying candidates.
A token is safe from that; which pull request was asked about is not.
The records of one server process form a hash chain. Each detail also holds the auditId, occurredAt, a
session id, seq (1, 2, …) and prevSha256: the SHA-256 of the previous record's detail string exactly as
written (64 zeros for a session's first record). To verify a file, walk it per session and check that seq
increases and each prevSha256 matches; McpBrickAuditChain.Verify in Ashlar.Mcp.Bricks does exactly that. An
edited, removed or reordered record breaks the chain. A gap in seq with the chain intact is a record the server
could not write. The chain cannot show records cut from the end of a session, a deleted file, or a session rewritten
from the edited record on: for the first, the server also logs each record's hash (RecordSha256) to stderr as it
writes it, so compare the last one logged with the last record; for the rest, keep a copy of the files elsewhere.
Configuration
| Variable | Meaning |
|---|---|
Ashlar__Mcp__Bricks__ExposedBrickIds__N |
Bricks to expose. An id with no registered brick stops the server at startup. |
Ashlar__Mcp__Bricks__MaxArgumentBytes |
Largest brick-call arguments object, in UTF-8 bytes (default 65536; 0 removes the cap). A larger call is refused, recorded, and not echoed back. |
Ashlar__Mcp__Server__ExposedToolIds__N |
Tools to expose (repo.fs.read, repo.fs.list). |
Ashlar__Mcp__Server__RepoRoot |
Root the repo tools read from (default: working directory). |
Ashlar__Audit__File__Directory |
Audit directory. |
Ashlar__Audit__File__MaxRotatedFiles |
How many audit files to keep; the oldest are deleted when a server starts. 0 (default here) keeps all of them. Each server start is one file, so a count also deletes old sessions' records. |
Ashlar__Audit__Sinks__0 |
File (default here), StructuredLog, or NoOp. |
Logs go to stderr; stdout carries only MCP JSON-RPC.
Known limits
- Host-registered bricks are not certificate-checked. The built-in bricks are admitted because this host
registers them, like any
RegisterBrick<T>()host. Use the certification gate for bricks you did not build. - No record, no result. A call returns only after its record is written and flushed. If the record cannot be
written (a full disk, an unwritable directory), the call comes back as a tool error saying so, with its
auditIdand without the brick's output, even when the brick ran. Flushed means handed to the operating system, notfsynced: a power loss can still take the last records. - A token passed as
githubTokentravels in the tool arguments, so the MCP client sees it. The audit record holds only its hash. The GitHub bricks send through Ashlar's egress guard (EG-HTTP-08), and the server refuses to start under theAirGappedprofile. - The caller identity is not in the audit record. Over stdio there is one local caller; the record holds what was asked and answered, not who asked.
- No local GGUF inference in this package. The
localmodel provider's native backend is left out of the tool package to keep the download small; build the host from source to get it.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.