Skip to main content
Glama

FORGE

Turn an HTTP API into an MCP server by writing a JSON file instead of a Python program.

You describe an API once: where it lives, how it authenticates, and which of its operations an agent should be allowed to call. FORGE reads that file and serves those operations as MCP tools. There is no per-vendor code. Adding a second API means writing a second JSON file, not writing a second server.

REST and GraphQL are both supported.

A whole server

This is a complete, working spec. Nothing else is needed to run it.

{
  "name": "acme",
  "transport": {
    "kind": "rest",
    "url": "https://api.acme.com/v2",
    "auth": {"kind": "bearer", "env": "ACME_TOKEN"}
  },
  "tools": [
    {
      "name": "find_customer",
      "description": "Look up a customer by email address.",
      "params": [
        {"name": "email", "type": "string", "required": true,
         "description": "Full email address, not a name or an account number."}
      ],
      "request": {"method": "GET", "path": "/customers", "query": {"email": "email"}}
    }
  ]
}
python forge.py acme.json --check    # validate it
python forge.py acme.json --list     # show the tools it would serve
python forge.py acme.json            # serve over stdio

Point an MCP client at that last command and it has a find_customer tool.

Related MCP server: graphql-mcp-server

Why a spec rather than a server

Every hand-written MCP server re-implements the same handful of things: build the auth headers, generate a JSON schema for each tool, render the request, handle the response. That code is dull, it is nearly identical each time, and it is not where anyone is looking when something goes wrong.

Putting it in a spec has two effects. Each new API becomes a description of that API instead of another copy of the plumbing. And a fix to the request builder is a fix to every server at once, rather than to whichever one you remember to go back to.

Parameter descriptions are part of the spec and end up in the tool's JSON schema. This matters more than it looks. The description is what the model reads when it decides what to put in a field, and a schema of bare types is how an agent ends up passing a business name where an id belongs.

Writes are held until confirmed

This is the part that is not boilerplate, and it is the reason the project exists.

Reads are cheap to get wrong. Writes are not. A tool marked "write": true is never sent on the first call. FORGE builds the exact request, shows it, hashes it, and stops:

This has NOT been sent yet.

POST https://api.acme.com/v2/customers/8891/cancel
  body:
    {
      "reason": "customer asked"
    }
  headers: Authorization, Content-Type, User-Agent

To send exactly this, call again with confirm="3f4ec46821fd2aa9".
Any change to the arguments invalidates the token.

Nothing reaches the API until that token comes back. The token is a hash of the rendered request rather than of the arguments or the caller's intent, which is what gives it its properties:

  • approving a change to record A cannot be replayed against record B

  • changing any field, including a free-text note, invalidates the token

  • a token is single use, so an approval authorises one send and not a standing licence

  • tokens expire, after five minutes by default

Header names appear in the preview. Header values never do. An Authorization value in a transcript is a leaked credential.

Writing a spec

Transport. kind is rest or graphql. url may contain ${ENV_VAR}, expanded at request time, so a tenant-specific endpoint does not have to be committed to the file.

Auth. none, bearer, basic, or header (an arbitrary named header). The credential is read from the environment by name, never stored in the spec. A missing credential refuses to build the request rather than sending an unauthenticated call and having the vendor log a 401.

Tools. Each has a name, a description, params, and either a request block (REST) or a query string with a variables list (GraphQL). Add "write": true to put it behind the gate.

Params. string, integer, number, boolean, array, object. Each takes required, default and description.

For REST, path may contain {param} placeholders, and query and body map API field names to parameter names. Path values are URL-escaped, so an id containing a slash cannot walk out of its endpoint and reach a different one.

Checking a spec

--check is the step before shipping. For GraphQL it parses each query and proves that the variables the query uses, the variables it declares, and the parameters exposed as tool arguments are all the same set. For REST it proves every path placeholder and every query and body mapping resolves to a real parameter.

These are the mistakes that otherwise surface on the first live call, which is not always a call you get to make privately.

The specs in this repo

Two are included so that FORGE can be run against something real without writing a spec first. They are examples, not the subject of the project.

Spec

Transport

Credentials

State

specs/greenhouse.json

REST

none

Works immediately. Two tests call it live.

specs/boulevard.json

GraphQL

env vars

Validated, never run against a live tenant.

greenhouse.json reads any company's public Greenhouse job board. It needs no credentials at all, which makes it the fastest way to confirm an install works.

boulevard.json describes the client-facing GraphQL API of a booking platform: 14 tools covering a cart booking flow through checkout, plus cancel and reschedule. Every operation, argument and type in it was taken from Boulevard's own MIT-licensed book-sdk (src/graph.d.ts) rather than from guesswork.

It has not been run against a live or sandbox tenant, because those credentials are issued through Boulevard's developer portal. --check passes on it and nothing has smoke-tested it. Its own instructions field says so, so a model loading the spec is not misled either.

Limits

  • stdio transport only, one spec per process

  • no pagination or retry helpers; a tool returns what the API returned

  • responses over 20,000 characters are truncated with an explicit notice rather than cut silently, because a model handed truncated JSON will usually invent the rest instead of reporting the problem

  • 30 second request timeout

Tests

pip install -r requirements.txt
python -m pytest test_forge.py -q

32 tests. Two of them call Greenhouse's live public API, so they need a network connection. The rest are offline, and the gate is tested by feeding it altered requests and expiring tokens rather than only working ones.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Turn any GraphQL endpoint into a set of MCP tools
    23
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Automatically generates MCP tools from any GraphQL API by introspecting its schema, supporting queries, mutations, and authentication.
    5
    GPL 3.0
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables users to expose arbitrary HTTP APIs as configurable MCP tools by defining tool specifications in a JSON configuration file.
    1,843 npm
    3
    -