ApiMcp.Dnx
1.3.4
dotnet tool install --global ApiMcp.Dnx --version 1.3.4
dotnet new tool-manifest
dotnet tool install --local ApiMcp.Dnx --version 1.3.4
#tool dotnet:?package=ApiMcp.Dnx&version=1.3.4
nuke :add-package ApiMcp.Dnx --version 1.3.4
API MCP Server
A Model Context Protocol server for calling HTTP APIs. Built on the official MCP C# SDK 2.0 (ModelContextProtocol) and .NET 10, over stdio. Use it as an API client and testing tool for your LLM tool-calling workflow: the LLM can send GET / POST / PUT / PATCH / DELETE / HEAD / OPTIONS / QUERY requests with custom headers, query strings and bodies — while secrets are sourced from environment variables on the server side and are never exposed to the model.
Quick start with dnx
Requires the .NET 10 SDK. Run the latest published package directly from NuGet.org:
dnx ApiMcp.Dnx@1.3.4 --yes
Configure it in your MCP client:
{
"mcpServers": {
"api": {
"command": "dnx",
"args": ["ApiMcp.Dnx@1.3.4", "--yes", "--secret-header", "X-Api-Key"],
"env": {
"APIMCP_SECRET_X_API_KEY": "your-secret-key"
}
}
}
}
Or with the Claude Code CLI, in one line:
claude mcp add api --scope user -e APIMCP_SECRET_X_API_KEY=your-secret-key -- dnx ApiMcp.Dnx@1.3.4 --yes --secret-header X-Api-Key
Configuration is read once at startup: after changing it, restart the MCP server (in Claude Code, start a new session). Call get_server_status to check what the running server loaded.
Tools
| Tool | Description | Parameters |
|---|---|---|
http_request |
Sends an HTTP request and returns status, headers, and body. | method (required), url (required), headers (optional JSON object), query (optional raw query string), body (optional), multipart (optional JSON object for file uploads), timeoutSeconds, followRedirects |
get_server_status |
Shows the running server's version, each secret header with its environment variable and whether it has a value (never the value), plain-override mode, and malformed configuration entries. | — |
list_secret_headers |
Lists the secret header names the server can fill from its environment (never the values). | — |
list_secret_query_params |
Lists the secret query-string parameter names the server fills from its environment (never the values). | — |
parse_openapi |
Parses an OpenAPI/Swagger JSON or YAML document into testable endpoints, including parameters, content types, schemas, and generated example request bodies. | url or specification (one required), headers (optional) |
http_request supports GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, and QUERY. QUERY is a safe, idempotent method for sending a query in the request body (like a read-only POST; RFC 9110 extension). Query strings are appended to the URL as-is; bodies default to application/json when they look like JSON. Responses are rendered as status line + response headers + body (bodies over 100 KB are truncated).
Multipart (file upload)
Use multipart to send multipart/form-data (single or multiple files) instead of a raw body (when set, body is ignored and the multipart Content-Type with its boundary overrides any you pass):
http_request(
method: "POST",
url: "https://example.com/upload",
multipart: {
"fields": {"title": "my doc"},
"files": [
{"field": "file", "path": "C:\\uploads\\a.png", "contentType": "image/png", "fileName": "a.png"},
{"field": "files", "contentBase64": "aGVsbG8gd29ybGQ=", "fileName": "hello.txt"}
]
}
)
Each file entry needs either path (a local file on the server) or contentBase64 (or contentBase64Url). field (default file), fileName, and contentType (inferred from the extension when omitted) are optional. Repeat the same field to upload multiple files under one name.
Paths are read with the server's own permissions. To limit which directories the server may read, set APIMCP_FILE_ROOTS to a semicolon- or newline-separated list of allowed directories; any path outside those roots is rejected.
OpenAPI / Swagger parsing
Use parse_openapi before testing an unfamiliar API. It accepts either a URL to swagger.json / openapi.json (or YAML) or the raw specification text.
For each endpoint it returns:
- HTTP method and path
- path/query/header parameters, including required flags and schema examples
- every request-body content type
- request-body schema, including referenced component schemas
- a generated
requestBody.examplesuitable for sending as thebodyargument ofhttp_request
Example:
parse_openapi(url: "https://example.com/swagger/v1/swagger.json")
For a JSON endpoint, use the returned example directly:
http_request(
method: "POST",
url: "https://example.com/api/orders",
headers: {"Content-Type": "application/json"},
body: "{...the generated requestBody.example...}"
)
The parser supports OpenAPI 3.x and Swagger 2.0 JSON/YAML through Microsoft.OpenApi.
Public Swagger test documents
Use the public Swagger Petstore documents to test parse_openapi without setting up your own API. Swagger's own documentation references the Petstore Swagger URL, and Swagger UI also uses a public Petstore OpenAPI definition as its example. citeturn0search0turn0search6
Swagger 2.0:
parse_openapi(url: "https://petstore.swagger.io/v2/swagger.json")
OpenAPI 3.x:
parse_openapi(url: "https://petstore3.swagger.io/api/v3/openapi.json")
End-to-end test:
1. parse_openapi(url: "https://petstore3.swagger.io/api/v3/openapi.json")
2. Select POST /pet
3. Take the returned requestBody.example
4. Call http_request(method: "POST", url: "<baseUrl>/pet", headers: {"Content-Type": "application/json"}, body: "<example>")
The OpenAPI specification is designed so tools can understand and interact with HTTP APIs with minimal implementation logic. citeturn0search11
Secret headers
Sensitive headers are declared server-side with a name → environment variable mapping. When the LLM sends a request that includes one of these header names, the server replaces the value with the one from the mapped environment variable. The model never sees the secret and cannot override it.
The simplest form is --secret-header NAME (repeatable). The value is read from APIMCP_SECRET_<NAME>, where the name is upper-cased and every non-alphanumeric character becomes _ (X-API-KEY → APIMCP_SECRET_X_API_KEY, Authorization → APIMCP_SECRET_AUTHORIZATION):
dnx ApiMcp.Dnx@1.3.4 --yes --secret-header X-API-KEY --secret-header Authorization
To choose the environment variable yourself, map it explicitly per-process:
dnx ApiMcp.Dnx@1.3.4 --yes --header-env "Authorization=MY_API_TOKEN" --header-env "X-Api-Key=MY_API_KEY"
or via the APIMCP_HEADER_ENV environment variable (semicolon-separated):
APIMCP_HEADER_ENV="Authorization=MY_API_TOKEN;X-Api-Key=MY_API_KEY"
The LLM discovers these names via list_secret_headers and simply includes them in the headers object of http_request; the running server injects the real value.
Malformed entries (no =, or an empty variable name such as X-API-KEY=) are ignored, logged to stderr at startup as a warning, and listed under "Configuration problems" by get_server_status. Some CLIs split -e NAME=VALUE at the second =, which truncates APIMCP_HEADER_ENV=X-API-KEY=MY_KEY; --secret-header avoids nested = entirely.
Plain override for test environments
By default a secret mapping always wins and the value the caller passes is ignored. For test environments where you want to pass header values directly from the tool parameters, start the server with plain override enabled:
dnx ApiMcp.Dnx@1.3.4 --yes --allow-plain-headers
# or: --allow-plain (enables both headers and query)
or via environment variable:
APIMCP_ALLOW_PLAIN_HEADERS=true
# or: APIMCP_ALLOW_PLAIN=true
Behavior when enabled:
- a non-empty value in
headersis sent as-is (no env lookup, so no error if the env var is unset) null(e.g.{"Authorization": null}) still injects the secret from the environment- omitting the header sends no header, as usual
Only use this for local/test setups. In production keep the default strict mode so secrets stay server-side.
Secret query parameters
Some APIs expect an API key on the query string (e.g. ?api_key=...). The same mapping pattern applies to query parameters, so the secret value is also never exposed to the model:
dnx ApiMcp.Dnx@1.3.4 --yes --query-env "api_key=MY_API_KEY"
or via the APIMCP_QUERY_ENV environment variable (semicolon-separated):
APIMCP_QUERY_ENV="api_key=MY_API_KEY"
The LLM discovers these names via list_secret_query_params, puts the name in the query argument of http_request (any value it passes is ignored), and the running server injects the real value.
For test environments, --allow-plain-query (or APIMCP_ALLOW_PLAIN_QUERY=true, or the shared --allow-plain / APIMCP_ALLOW_PLAIN=true) works the same way: a non-empty value in query is kept as-is, while an empty value (e.g. query: "api_key=") injects the secret.
Build & Run
cd ApiMcp
dotnet build
# Windows Auth-free local run
dotnet run -- --header-env "Authorization=MY_API_TOKEN"
Logs go to stderr only (stdout is reserved for MCP JSON). Secret values never appear in logs.
Requirements
- .NET SDK 10 (for building / running from source)
- Optional: Native AOT publish needs a Visual Studio C++ workload — see below.
Publish
Managed single-file (framework-dependent)
cd ApiMcp
dotnet publish -c Release -r win-x64 /p:PublishAot=false /p:PublishSingleFile=true --self-contained false -o .\\bin\\publish-single
Native AOT (fast startup, no runtime install)
$env:PATH = "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer;" + $env:PATH
dotnet publish -c Release -r win-x64 -o .\\bin\\publish-aot
NuGet publishing
Pushing a v* tag triggers the .github/workflows/dnx.yml workflow, which packs ApiMcp.Dnx and publishes it to NuGet.org using OIDC trusted publishing (no API key stored in the repo). You can also trigger it manually via Actions → Run workflow with an optional package_version.
Testing example (public CRUD API with auth)
DummyJSON is a free API with JWT auth and full CRUD — ideal for trying the server. emilys / emilyspass is a public demo account.
- Login to get an access token:
http_request(
method: "POST",
url: "https://dummyjson.com/auth/login",
headers: {"Content-Type": "application/json"},
body: "{\"username\":\"emilys\",\"password\":\"emilyspass\"}"
)
The response body contains accessToken (and refreshToken). Use it as a Bearer token in the next calls.
- Call an auth-protected endpoint:
http_request(
method: "GET",
url: "https://dummyjson.com/auth/me",
headers: {"Authorization": "Bearer eyJhbGciOi..."}
)
- Update an existing product (PUT):
http_request(
method: "PUT",
url: "https://dummyjson.com/auth/products/1",
headers: {"Content-Type": "application/json", "Authorization": "Bearer eyJhbGciOi..."},
body: "{\"price\":101}"
)
- Delete it (DELETE):
http_request(
method: "DELETE",
url: "https://dummyjson.com/auth/products/1",
headers: {"Authorization": "Bearer eyJhbGciOi..."}
)
Tip: because the token is dynamic, it is fine to pass it literally. For a fixed secret (e.g. a GitHub PAT), prefer a secret header mapping so the value never reaches the model:
dnx ApiMcp.Dnx@1.3.4 --yes --header-env "Authorization=MY_GITHUB_PAT"
# then call: http_request(method: "GET", url: "https://api.github.com/user", headers: {"Authorization": "ignored"})
Running over dnx (no install)
dnx ApiMcp.Dnx@1.3.4 --yes
License
| 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.