mcp-from-api

mcp-from-api

An MCP server that exposes any REST API to LLMs through runtime discovery, providing tools to browse endpoints, fetch schemas, and apply business rules without hardcoding or schema duplication.

Category
访问服务器

README

API-First SaaS + MCP: A Pattern Guide

How to expose any REST API to an LLM — without duplicating schemas, hardcoding rules, or rebuilding your backend.

This repo is a working reference implementation of the pattern (Python + Flask + FastMCP), but the pattern itself is language-agnostic. The concepts in the first half apply equally to a Node.js Express API, a Rails app, a Go service, or any HTTP backend.

User (natural language)
  → LLM
    → MCP Server       (thin adapter — fetches everything from the API at runtime)
      → Your REST API  (unchanged business logic)
        → Your database

The Problem

Most approaches to "LLM + API" land in one of two failure modes:

Approach Problem
Hardcode everything in the system prompt Schemas drift, rules go stale, every API change needs an LLM update
Give the LLM raw OpenAPI spec All schemas in context every turn — most irrelevant, tokens wasted
One MCP tool per endpoint 25 endpoints = 25 tool schemas in context always

The root issue: knowledge duplication. The API already knows its own shape. The goal is to make the LLM ask the API what it can do, rather than telling the LLM upfront.


Why MCP Over Direct Function Calling?

You could skip MCP entirely and pass your OpenAPI spec to the LLM as a tool list. Many integrations do this. Here's why that diverges from this pattern at scale:

Direct function calling MCP + discovery
Schema in context All tools loaded every turn Fetched on demand — only what's needed
Client portability Tied to one LLM client Any MCP-compatible client (Claude Desktop, Cursor, custom agents)
API change propagation Regenerate and redeploy the tool list Discovery routes return the latest state automatically
Business rules In the system prompt or tool descriptions In the rules registry, fetched at runtime
MCP server updates N/A Never needed — server has no API knowledge

MCP also gives you a transport-agnostic boundary: the same MCP server works over stdio (subprocess), HTTP/SSE, or WebSocket, and works with any LLM that supports tool use — not just the one you started with.

The short version: direct function calling works for simple APIs and single clients. MCP with discovery works for APIs that change and systems that need to evolve.


The Pattern

Three layers, each with a single job.

Running example: this guide uses a recruiting CRM throughout — an API with four entities: accounts (client companies), contacts (hiring managers), candidates (job seekers), and placements (candidate ↔ account relationships at a given stage). It's a good fit because the entities have real dependencies: you can't create a placement without both a candidate and an account. Wherever you see "recruiting CRM" or "placement", substitute your own domain.

Layer 1 — Your API (add a registry + discovery routes)

Your existing business logic stays untouched. You add two things:

A metadata registry — a structured description of every endpoint, stored in one place. Not used for routing. Used only to answer the question "what can you do, and how?"

Each entry in the registry holds:

  • path and method
  • description — plain English, searchable by the LLM
  • body_schema — what the endpoint accepts
  • response_schema — what the endpoint returns
  • tags — domain grouping (accounts, candidates, placements…)

Four discovery endpoints — meta-routes that expose the registry over HTTP:

Route Returns
GET /api/meta/endpoints?tag= All endpoints, optionally filtered by domain
GET /api/meta/endpoints/search?q= Endpoints matching a keyword
GET /api/meta/endpoints/detail?path=&method= Full schema for one endpoint
GET /api/meta/rules?entity=&action= Dependency rules and validations

These four routes are the only contract between your API and the MCP layer. They don't change your existing routes.

Generating the registry. If your framework already produces an OpenAPI spec — FastAPI, NestJS, Spring Boot, Django REST Framework, Gin, Fastify — write a one-time converter that reads /openapi.json and writes your registry file. You get paths, methods, parameters, and response schemas for free. The one thing no tool can generate is the rules registry: business rules like "a placement requires a candidate" are domain knowledge that must be authored by hand.

Registry as a team contract. When the API team and the AI/LLM team are different people, the registry + discovery routes become a formal handoff point. The AI team can build and test the entire MCP layer against the four discovery routes — without knowing anything about how the backend is implemented, what database it uses, or how routes are structured internally. Any breaking change to the registry is immediately visible to the LLM layer without any coordination overhead.


Layer 2 — The MCP Server (a thin discovery adapter)

The MCP server owns zero API knowledge. No schemas, no hardcoded paths, no business rules. It exposes six tools that the LLM calls on demand:

Tool What it does
list_domains() (optional) Domain map with endpoint counts — use for 50+ endpoint APIs or when search returns too many unrelated results
list_api_endpoints(tag?) Browse available endpoints by domain
search_api_endpoints(keyword, tag?) Find the right endpoint — returns scored results ranked by relevance
get_api_endpoint_details(path, method) Read the full input + output schema
get_workflow_rules(entity, action) Fetch dependency rules before any write
call_api(path, method, params?, body?) Execute the actual call

The LLM discovers what it needs, when it needs it. Adding an endpoint to your API makes it immediately discoverable — no changes to the MCP server.

Why 5 tools instead of one per endpoint:

With 25 endpoints, registering each as a separate MCP tool loads all 25 schemas into context on every turn — most irrelevant. With 5 meta-tools, the LLM pays schema cost only for the endpoints relevant to the current task.

User: "show me all active candidates"

LLM → search_api_endpoints("active candidates")
   ← [{path: "/api/candidates", method: "GET", description: "List all candidates..."}]

LLM → get_api_endpoint_details("/api/candidates", "GET")
   ← {query_params: {status: "active|passive|placed|inactive"}, response_schema: {...}}

LLM → call_api("/api/candidates", "GET", {status: "active"})
   ← {candidates: [...], total: 3}

3 small tool calls. Zero schemas for unrelated endpoints loaded into context.


Layer 3 — Rules and Restrictions

This is the most commonly skipped part of the pattern, and the one that matters most for complex APIs.

Every API has implicit business rules. In a recruiting CRM:

  • You can't create a placement without a candidate and an account
  • Deleting an account cascades to all its contacts
  • A placement stage must follow a valid progression

If you put these rules in the LLM's system prompt, you now have two sources of truth. The system prompt drifts. Rules go stale when the API changes. The LLM "remembers" rules it was told, not rules the API actually enforces.

The solution: a rules registry, owned by the API.

A rules registry maps every entity + action to:

  • dependencies — what must exist before this action, and how to verify it
  • validations — field constraints the LLM should enforce
  • cascade — what else gets affected
  • order_hint — the correct creation order for multi-step operations

What one entry looks like (language-agnostic JSON structure):

{
  "placements": {
    "create": {
      "dependencies": [
        {
          "entity":      "candidates",
          "field":       "candidate_id",
          "required":    true,
          "verify_with": "GET /api/candidates/{candidate_id}",
          "reason":      "Placement must reference an existing candidate",
          "if_missing":  "Create candidate first via POST /api/candidates"
        },
        {
          "entity":      "accounts",
          "field":       "account_id",
          "required":    true,
          "verify_with": "GET /api/accounts/{account_id}",
          "reason":      "Placement must reference an existing account",
          "if_missing":  "Create account first via POST /api/accounts"
        }
      ],
      "validations": [
        "stage must be one of: sourced, screening, interviewing, offered, placed, rejected"
      ],
      "order_hint": "accounts → candidates → (contacts optional) → placements"
    },
    "delete": {
      "dependencies": [],
      "cascade": [],
      "order_hint": "safe to delete — no child records depend on placements"
    }
  }
}

The structure is just data — store it as JSON, YAML, a module-level dict, a database table, whatever fits your stack. The only requirement is that it's readable by the /api/meta/rules route.

The LLM calls get_workflow_rules before any write. It gets back the exact checks to run. If a dependency is missing, it returns missing_dependency and stops — it never proceeds.

User: "place Alice at Acme Corp"

LLM → get_workflow_rules("placements", "create")
   ← dependencies: [candidate_id required, account_id required]
   ← order_hint: "accounts → candidates → placements"

LLM → search for "Alice" in candidates  ✓  found: id=1
LLM → search for "Acme Corp" in accounts  ✗  not found

LLM returns:
{
  "status": "missing_dependency",
  "summary": "Acme Corp does not exist as an account. Create it before the placement.",
  "suggestions": ["Create account 'Acme Corp', then retry the placement"]
}

Key principle: rules live in one place, fetched at runtime. Updating a rule requires changing only the registry — no LLM infrastructure changes.

Approach Rule update requires
Hardcoded in system prompt Edit the CLI / agent config, redeploy
Dynamic via rules registry Edit the registry file — nothing else

Full Write-Path Sequence

This is what the complete flow looks like for any create, update, or delete operation — combining Layer 2, Layer 3, and Layer 4:

User: "create a placement for Alice at Acme Corp"
│
├─ 1. DISCOVER
│     LLM → search_api_endpoints("create placement")
│          ← {path: "/api/placements", method: "POST", description: "..."}
│
│     LLM → get_api_endpoint_details("/api/placements", "POST")
│          ← {body_schema: {candidate_id, account_id, stage, ...}}
│
├─ 2. CHECK RULES
│     LLM → get_workflow_rules("placements", "create")
│          ← {dependencies: [candidate_id required, account_id required],
│              order_hint: "accounts → candidates → placements"}
│
├─ 3. VERIFY DEPENDENCIES
│     LLM → call_api("/api/candidates/search", "GET", {q: "Alice"})
│          ← found: {id: 1, name: "Alice Johnson"}  ✓
│
│     LLM → call_api("/api/accounts", "GET", {q: "Acme Corp"})
│          ← not found  ✗
│          → return missing_dependency, stop here
│
│   (if all dependencies exist, continue ↓)
│
├─ 4. HUMAN APPROVAL
│     Agent intercepts the call before it reaches the API
│     ╭─ Approval Required ─────────────────────────╮
│     │  POST /api/placements                        │
│     │  {"candidate_id": 1, "account_id": 5, ...}  │
│     ╰─────────────────────────────────────────────╯
│     n → inject rejection into LLM context, stop
│     y → continue ↓
│
└─ 5. EXECUTE + RESPOND
      LLM → call_api("/api/placements", "POST", body)
           ← {placement: {id: 12, stage: "sourced", ...}}

      LLM returns structured envelope:
      {"status": "success", "summary": "Placement created...", "data": {...}}

Each step has a clear exit condition. The LLM never guesses — it checks rules, verifies existence, then acts.


Layer 4 — The Agent (human-in-the-loop + structured output)

Two more patterns at the LLM layer that complete the system:

Human-in-the-loop at the infrastructure layer.

Write operations are intercepted by the agent framework before they hit the API. The LLM calls the tool directly — the framework shows the approval prompt. The LLM never asks "shall I proceed?" in conversation (that causes a duplicate prompt). The gate is in the infrastructure, not the conversation.

╭─ Approval Required ──────────────────────────────────╮
│  Method  : POST                                       │
│  Path    : /api/accounts                              │
│  Payload : {"name": "Acme Corp"}                      │
╰───────────────────────────────────────────────────────╯
  Proceed? y/n:

Read-only calls and discovery tools pass through without prompting.

Structured output envelope.

All responses follow a consistent shape, regardless of success or failure:

{
  "status":      "success | error | out_of_scope | missing_dependency",
  "action":      "short description of what was done",
  "data":        {},
  "summary":     "one-sentence plain English summary",
  "suggestions": ["follow-up action 1", "follow-up action 2"]
}

out_of_scope is returned when the LLM searches and finds no matching endpoints — no fabricated tool calls, no hallucinated routes.


How to Apply This to Your Own API

Step 1 — Build a metadata registry. In whatever language your API uses, create a data structure (file, module, table) that holds a description of every endpoint: path, method, input schema, output schema, tags. This is metadata only — it doesn't affect routing.

Step 2 — Add four discovery routes. Add the four meta-routes to your API. They read from the registry at runtime. No changes to existing business logic.

Step 3 — Define a rules registry. For each entity and each action (create, update, delete), define what must exist first and what validations apply. Store this alongside the endpoint registry, in the same file or module.

Step 4 — Write a thin MCP server. Five tools: list, search, detail, rules, call. The MCP server has no API knowledge — it proxies the discovery routes and executes calls. It can be written in any language that has an MCP SDK (Python, TypeScript, Go, Kotlin…). It doesn't change when your API changes.

Step 5 — Configure the LLM agent. The system prompt is short because rules aren't in it. Instruct the LLM to: search → inspect schema → check rules → get approval → call. The workflow is mechanical, not conversational.


What This Pattern Doesn't Solve

Being clear about the limits helps you decide whether this pattern fits your situation:

Registry maintenance overhead. The registry is only as good as what you put in it. If descriptions are vague or response_schema is missing, the LLM will make poor tool calls. Keeping the registry accurate as the API evolves requires discipline — it won't self-update from your route handlers.

Latency per turn. Each LLM turn involves 2–4 HTTP calls to the discovery routes before the actual API call. For high-frequency or latency-sensitive use cases, the MCP server should cache the registry in memory at session start rather than fetching it fresh each call.

The rules registry doesn't enforce — it informs. The rules registry tells the LLM what to check. It doesn't prevent a bad API call if the LLM ignores it. Real enforcement still lives in the API itself (foreign key constraints, validation logic). The registry is a guide layer, not a hard gate.

This pattern doesn't replace API authentication or authorization. The MCP server calls the API with whatever credentials it has. If your API has per-user permissions, those must be enforced at the API layer — the MCP server is transparent to them.


This Implementation (Python)

The recruiter CRM is a reference implementation of the pattern. Four entities:

accounts  ──< contacts      (one account → many contacts)
accounts  ──< placements    (one account → many placements)
candidates──< placements    (one candidate → many placements)
contacts  ──< placements    (one contact manages many placements, optional)

Stack: Flask (API), FastMCP (MCP server), OpenRouter (LLM), SQLite (database), Rich (terminal UI)

File Structure

MCP-from-API/
├── src/
│   ├── registry.py         ← ENDPOINT_REGISTRY + RULES_REGISTRY (single source of truth)
│   ├── recruiter_api.py    ← Flask routes + DB logic (imports registry, no metadata here)
│   ├── mcp_server.py       ← 6 MCP tools, zero hardcoded API knowledge
│   └── chat.py             ← CLI: OpenRouter LLM, human-in-the-loop, JSON output
├── docs/
│   ├── api.md              ← endpoint reference + schema tables
│   ├── mcp.md              ← MCP design + tool details + improvement ideas
│   └── cli.md              ← CLI usage guide + example queries
├── requirements.txt        ← flask, fastmcp, httpx, python-dotenv, rich
├── .env.example            ← template — copy to .env and fill in your key
└── recruiter.db            ← SQLite (auto-created, seeded on first run only)

Quick Start

# 1. Install dependencies
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt

# 2. Configure
cp .env.example .env
# Edit .env — set OPENROUTER_API_KEY=sk-or-...

# 3. Start the API  (terminal 1)
.venv/bin/python src/recruiter_api.py

# 4. Start the CLI  (terminal 2)
.venv/bin/python src/chat.py

Configuration

OPENROUTER_API_KEY=sk-or-...
OPENROUTER_MODEL=openai/gpt-4o-mini
MCP_SCRIPT=mcp_server.py

Adding a New Endpoint

  1. Add the route to recruiter_api.py with @app.route
  2. Call register(...) in registry.py with body_schema and response_schema
  3. If the entity has dependencies, add an entry to RULES_REGISTRY in registry.py

No changes needed to mcp_server.py or chat.py.


Further Reading

  • docs/api.md — endpoint reference, schema tables, data model
  • docs/mcp.md — MCP design decisions, tool details, improvement ideas
  • docs/cli.md — CLI usage guide, example queries, troubleshooting

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选