使用说明
mcp-http-tools
Turn any HTTP API into MCP tools via YAML config. No code required.
Define your tools in a YAML file — each one maps to an HTTP request. The MCP server reads the config, exposes the tools, and proxies requests to your APIs.
Quick start
From source
pnpm install
From GitHub Packages
Releases are published to GitHub Packages as @3h4x/mcp-http-tools. To install, tell your package manager to fetch the @3h4x scope from GitHub's registry by adding an .npmrc:
@3h4x:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
GITHUB_TOKEN must be a personal access token with the read:packages scope. Then:
pnpm add @3h4x/mcp-http-tools
Create ~/.config/mcp-http-tools/config.yaml:
tools:
- name: check_health
description: Check if the API is healthy
url: http://localhost:3000/health
response:
type: text
- name: query_metrics
description: Run a PromQL query
url: http://localhost:9090/api/v1/query
params:
- name: query
description: PromQL expression
required: true
response:
type: json
path: data.result
Run the server:
node index.js
To use a different config file for one run:
node index.js --config /path/to/config.yaml
If the explicit config file is missing or invalid YAML, startup fails instead of falling back to an empty tool list.
HTTP transport
By default the server speaks MCP over stdio. Pass --http to serve it over the network instead, using MCP's Streamable HTTP transport — the same mechanism claude mcp add --transport http <url> targets.
MCP_HTTP_TOKEN=some-long-random-token MCP_HTTP_PORT=3000 node index.js --http --config /path/to/config.yaml
- Listens on
<MCP_HTTP_HOST>:<MCP_HTTP_PORT>(default127.0.0.1:3000), serving the MCP endpoint at/mcp. The loopback default is deliberate for a bare-host deploy behind a local reverse proxy; setMCP_HTTP_HOST=0.0.0.0when running in a container, where a Service/proxy routes to the container's own IP rather than the host's loopback. MCP_HTTP_TOKENis required when--httpis passed; startup fails otherwise. Every request must sendAuthorization: Bearer <MCP_HTTP_TOKEN>(or anaccesstoken, or an OAuth access token, see OAuth sign-in) or it gets a401.- Stateless mode: no session is kept between requests.
Config reference
Each tool supports:
| Field | Required | Default | Description |
|---|---|---|---|
name |
yes | MCP tool name | |
description |
no | "" |
Shown to the LLM |
url |
yes | Target HTTP endpoint. Supports {param} placeholders and {+path} raw path placeholders, including hyphenated names like {user-id} |
|
method |
no | GET |
HTTP method (GET, POST, PUT, PATCH, or DELETE) |
auth.bearer_env |
no | Shorthand for Authorization: Bearer ${ENV_VAR}. The value must be an environment variable name containing only letters, digits, and underscores |
|
headers |
no | Static headers. Supports ${ENV_VAR} substitution |
|
params |
no | [] |
Tool input parameters (see below) |
response.type |
no | text |
text (raw) or json (parsed) |
response.path |
no | Dot-path to extract from JSON (e.g. data.result) |
|
response.template |
no | JSON-only output template using {dot.path} placeholders (e.g. Status: {status}) |
|
timeout |
no | 30000 |
Request timeout in milliseconds |
retry.count |
no | 2 when retry is set |
Number of retry attempts after the first request. Retries are disabled unless a retry object is present |
retry.backoff_ms |
no | 250 when retry is set |
Initial exponential backoff delay in milliseconds. The delay doubles after each failed attempt |
When a tool entry is present, it must be an object using only the documented fields above. Unsupported top-level tool keys are rejected at startup.
Retry
Retries are opt-in per tool. Add a retry object to retry transient failures:
retry:
count: 2
backoff_ms: 250
count is the number of retry attempts after the first request. backoff_ms is the initial delay, so 250 waits 250ms before the first retry and 500ms before the second. Each request attempt gets the configured timeout; the timeout is not shared across all attempts.
Retried failures are HTTP 408, 429, 500, 502, 503, and 504, request timeouts, and network errors reported by fetch.
Params
| Field | Required | Default | Description |
|---|---|---|---|
name |
yes | Parameter name | |
description |
no | Shown to the LLM | |
type |
no | string |
JSON Schema type (string, number, integer, boolean, array, or object) |
enum |
no | Non-empty list of allowed values. When type is set, every enum value must match it |
|
required |
no | false |
Whether the LLM must provide this |
default |
no | Value used when param is omitted. It must match the effective param type, and if enum is set it must also be one of those values |
|
pattern |
no | Regular expression a string value must match in full (anchored as ^(?:pattern)$). Enforced when the tool is called, and advertised to clients in the schema |
|
maxLength |
no | Maximum length of a string value. Enforced when the tool is called | |
minimum |
no | Inclusive lower bound for a number/integer value. Enforced when the tool is called |
|
maximum |
no | Inclusive upper bound for a number/integer value. Enforced when the tool is called |
When a param entry is present, it must be an object using only the fields above. Unsupported param keys are rejected at startup.
Argument validation
The MCP SDK does not validate arguments against the advertised schema, so this server does. enum, pattern, maxLength, minimum and maximum are enforced on every call: a violating argument fails the call before any HTTP request is made.
Set strict: true on a tool (or strict_args: true at the top of the config, for every tool) to additionally reject unknown arguments, wrong types and missing required arguments. Unknown arguments matter for GET tools, because they are appended to the query string: without strict_args, a caller could add a second query= next to one the config pinned in the URL.
Together with {param} placeholders in the URL, pattern lets a tool expose a narrow slice of an API without a proxy. The fixed part of the request lives in the URL, and the caller can only fill a validated hole:
strict_args: true
tools:
- name: search_glasspad_logs
url: http://loki:3100/loki/api/v1/query_range?query=%7Bnamespace%3D%22glasspad%22%7D%20%7C%3D%20%22{contains}%22
params:
- name: contains
pattern: "[A-Za-z0-9 _.:-]*" # no quote, backslash or brace: cannot break out of the LogQL string
maxLength: 80
default: ""
Access tokens
MCP_HTTP_TOKEN is the unrestricted token: it sees every tool. Add an access list to give other callers their own token and a tool allowlist:
access:
- name: maciej
token_env: MCP_TOKEN_MACIEJ # env var holding the token, at least 32 characters
tools: [search_glasspad_logs]
The token alone selects the scope, on the same /mcp endpoint. tools/list returns only that principal's tools, and calling any other tool answers Unknown tool, exactly as if it did not exist. Startup fails if a listed token's env var is unset, shorter than 32 characters, or identical to another principal's token, so a scoped principal can never silently disappear or fall open. Each call by a scoped principal logs principal=<name> tool=<tool> to stderr (never arguments).
How params map to requests
- GET: params become URL query parameters
- POST: params become keys in a JSON body
- URL placeholders:
{param}in the URL consumes the param value (not sent as query param or body key). If that param isrequired: true, omitting it fails the request instead of inserting an empty path segment. - Raw path placeholders:
{+path}preserves/separators while still encoding each path segment - Raw path safety:
{+path}rejects empty segments and./..segments so callers cannot escape the configured URL prefix - Raw path config contract: params used by
{+path}must berequired: trueor have a safe non-emptydefault - Placeholder names: placeholder matching uses the param
name, so names like{user-id}and{+file-path}are valid
Response shaping
responsemust be an object when set. Unsupported keys are rejected at startup.response.type: textpasses the upstream body through unchanged.response.type: jsonpretty-prints parsed JSON by default.response.pathextracts one dot-path from parsed JSON. Missing paths fall back to the raw upstream body.response.templateformats parsed JSON with{dot.path}placeholders. Missing placeholders are left unchanged so config mistakes stay visible.- When both
response.pathandresponse.templateare set, the template runs against the extracted subtree.
See docs/raw-path-placeholders.md for the exact {+path} contract.
Composite tools
A requests array turns a tool into a fan-out: it makes every request in the array in parallel and returns one merged JSON object, keyed by each request's key. Use it for "give me the whole picture" tools -- e.g. a project overview that pulls k8s state, logs, metrics, and error data in a single call instead of five round-trips.
- A tool has either
url(a single request) orrequests(a composite) -- never both. requestsmust be a non-empty array. Each entry needs a uniquekeyplus its ownurl/method/headers/params/response/timeout/retry-- exactly the same fields a regular tool would use, just nested one level down.- The composite tool's own top-level
paramsbuild the MCPinputSchemacallers see (same as any tool). Each request re-declares the params it actually needs in its ownurl/query string -- there's no implicit inheritance, so a request only sees the args it explicitly lists. - One request failing (timeout, non-2xx, network error) does not fail the others. Its key gets
{ "error": "..." }in the merged result instead -- partial data beats an all-or-nothing failure for an overview tool. - Each request's own
responseshaping still applies before merging; aresponse.type: jsonresult is embedded as a real nested object, not a JSON string.
tools:
- name: project_overview
description: Pods, recent logs, and error count for one project, in one call
params:
- name: namespace
required: true
requests:
- key: pods
url: https://127.0.0.1:6443/api/v1/namespaces/{namespace}/pods
params:
- { name: namespace, required: true }
headers:
Authorization: "Bearer ${K3S_API_TOKEN}"
response: { type: json }
- key: recent_errors
url: https://errors.example.com/api/projects/{namespace}/issues/
params:
- { name: namespace, required: true }
auth: { bearer_env: GLITCHTIP_TOKEN }
response: { type: json, path: results }
Calling project_overview with { "namespace": "bonker" } returns:
{
"pods": { "kind": "PodList", "items": [ ... ] },
"recent_errors": [ { "title": "...", "count": 3 }, ... ]
}
OAuth sign-in
An oauth: block lets MCP clients that sign users in with OAuth (for example a hosted connector that asks for an OAuth Client ID) call the --http transport without a static token. The server stays a resource server only: users sign in at your OpenID Connect provider, and the server checks the access token it gets.
oauth:
issuer: https://id.example.com # the provider's issuer (iss)
audience: my-mcp # the client id registered there; must be in the token's aud
resource: https://mcp.example.com/mcp # the public URL of this server's /mcp endpoint
allowed_emails: [you@example.com] # who may sign in
# jwks_uri: https://id.example.com/.well-known/jwks.json (this is the default)
# userinfo_uri: https://id.example.com/userinfo (used when the token has no email claim)
# tools: [some_tool] # optional allowlist, like an access entry
GET /.well-known/oauth-protected-resource(and the same path followed by the resource path, e.g./.well-known/oauth-protected-resource/mcp) returns RFC 9728 metadata namingissueras the authorization server. No token is needed.- A request with no valid credentials gets
401withWWW-Authenticate: Bearer resource_metadata="<that URL>", which is how a client finds the provider. - Accepted tokens: RS256 JWTs signed by a key in the provider's JWKS, with
iss=issuer,audcontainingaudience, a validexp/nbf(60 s clock skew), and an e-mail onallowed_emails(case-insensitive). The e-mail comes from the token'semailclaim, else fromuserinfo_uri(itssubmust match). The JWKS is cached for 10 minutes and refetched at most once a minute for an unknownkid. MCP_HTTP_TOKENandaccesstokens keep working alongside it. Everyissuer,resource,jwks_urianduserinfo_urimust be https.- If your provider has no dynamic client registration, register a public client with PKCE yourself and enter its id in the MCP client.
Examples
GET with query params
- name: search_logs
description: Search logs via LogQL
url: http://localhost:3100/loki/api/v1/query_range
retry:
count: 2
backoff_ms: 250
params:
- name: query
description: LogQL query
required: true
- name: direction
description: Query direction
enum: [forward, backward]
default: backward
- name: limit
default: "50"
response:
type: json
path: data.result
POST with JSON body
- name: create_alert
description: Create an alert silence
method: POST
url: http://localhost:9093/api/v2/silences
params:
- name: matchers
required: true
- name: comment
required: true
response:
type: json
URL path parameters
- name: get_label_values
description: List values for a Loki label
url: http://localhost:3100/loki/api/v1/label/{label}/values
params:
- name: label
description: Label name (e.g. app, job)
required: true
response:
type: json
path: data
JSON response template
- name: summarize_build
description: Summarize the latest build result
url: http://localhost:3000/builds/latest
response:
type: json
template: "Build {id}: {status} ({timing.duration_ms} ms)"
Auth via environment variable
- name: list_alerts
description: List active alerts
url: http://localhost:9093/api/v2/alerts
auth:
bearer_env: ALERTMANAGER_TOKEN
response:
type: json
Explicit headers.Authorization still wins if you need a non-Bearer scheme or a fully custom value.
Config location
Config is loaded from (first found wins):
~/.config/mcp-http-tools/config.yaml./config.yaml(repo root)
If neither exists, the server starts with no tools.
Pass --config /path/to/config.yaml or --config=/path/to/config.yaml to override the search path and load exactly one file. An explicit override is required: if that file is missing or invalid YAML, the server exits with an error.
Use with Claude Desktop
Via supergateway for SSE transport:
pnpm dlx supergateway --stdio "node /path/to/mcp-http-tools/index.js --config /path/to/config.yaml" --port 9191
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"mcp-http-tools": {
"url": "http://localhost:9191/sse"
}
}
}
Use with Claude Code
Add to .claude/settings.json or use as a stdio MCP server:
{
"mcpServers": {
"mcp-http-tools": {
"command": "node",
"args": ["/path/to/mcp-http-tools/index.js", "--config", "/path/to/config.yaml"]
}
}
}
Tests
pnpm test
Pushes to main and pull requests also run pnpm test in GitHub Actions.
Releasing
main is released by GitHub Actions via gha-semantic-tag-vanilla, a zero-dependency drop-in for semantic-release that reads the same .releaserc. Conventional commits on main determine the next version, create the GitHub release, and publish the package to npm. The same workflow can also be triggered manually from GitHub Actions when a maintainer needs to rerun a release on main.
The release workflow publishes to GitHub Packages with the workflow's own GITHUB_TOKEN (passed as NPM_TOKEN), so the job needs packages: write permission; no npm token secret is involved.
Stack
- Node.js ESM
- @modelcontextprotocol/sdk — MCP protocol
- js-yaml — config parsing
License
MIT
