HTTP API转MCP工具服务

通过YAML配置即可将任意HTTP API转换为标准MCP工具的开发服务,无需编写代码,让AI助手能够直接调用现有REST接口。

  • by 3h4x
  • JavaScript
  • 混合部署
  • 更新于
在 GitHub 查看 3h4x/mcp-http-tools

使用说明

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> (default 127.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; set MCP_HTTP_HOST=0.0.0.0 when running in a container, where a Service/proxy routes to the container's own IP rather than the host's loopback.
  • MCP_HTTP_TOKEN is required when --http is passed; startup fails otherwise. Every request must send Authorization: Bearer <MCP_HTTP_TOKEN> (or an access token, or an OAuth access token, see OAuth sign-in) or it gets a 401.
  • 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 is required: 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 be required: true or have a safe non-empty default
  • Placeholder names: placeholder matching uses the param name, so names like {user-id} and {+file-path} are valid

Response shaping

  • response must be an object when set. Unsupported keys are rejected at startup.
  • response.type: text passes the upstream body through unchanged.
  • response.type: json pretty-prints parsed JSON by default.
  • response.path extracts one dot-path from parsed JSON. Missing paths fall back to the raw upstream body.
  • response.template formats parsed JSON with {dot.path} placeholders. Missing placeholders are left unchanged so config mistakes stay visible.
  • When both response.path and response.template are 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) or requests (a composite) -- never both.
  • requests must be a non-empty array. Each entry needs a unique key plus its own url/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 params build the MCP inputSchema callers see (same as any tool). Each request re-declares the params it actually needs in its own url/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 response shaping still applies before merging; a response.type: json result 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 naming issuer as the authorization server. No token is needed.
  • A request with no valid credentials gets 401 with WWW-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, aud containing audience, a valid exp/nbf (60 s clock skew), and an e-mail on allowed_emails (case-insensitive). The e-mail comes from the token's email claim, else from userinfo_uri (its sub must match). The JWKS is cached for 10 minutes and refetched at most once a minute for an unknown kid.
  • MCP_HTTP_TOKEN and access tokens keep working alongside it. Every issuer, resource, jwks_uri and userinfo_uri must 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):

  1. ~/.config/mcp-http-tools/config.yaml
  2. ./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

License

MIT