Graft

Graft

Enables building agent-ready APIs that expose tools as both HTTP and MCP endpoints from a single server definition, with automatic OpenAPI, discovery docs, and interactive API reference.

Category
访问服务器

README

Graft 🌱

Build agent-ready APIs without splitting your server model.

Define tools once, then expose them as both HTTP endpoints and MCP tools from the same server. Graft also generates discovery docs, OpenAPI, and an interactive API reference automatically.

import { createApp } from '@schrepa/graft'

const app = createApp()

app.tool('lookup_user', {
  description: 'Look up a user by id.',
  auth: true,
  inputSchema: {
    type: 'object',
    properties: {
      id: { type: 'string' },
    },
    required: ['id'],
  },
  handler: ({ id }) => ({ id, found: true }),
})

export default app

That one definition gives you:

  • POST /mcp — MCP endpoint (Streamable HTTP). Agents connect here.
  • GET /lookup-user?id=123 — HTTP endpoint. Any client calls the same tool as REST.
  • /.well-known/agent.json — Agent discovery. Tools, resources, capabilities.
  • /.well-known/mcp.json — MCP server card. Protocol version and transport URL.
  • /openapi.json — Auto-generated OpenAPI 3.1 spec.
  • /docs — Interactive API reference (Scalar).
  • /health — Health check with tool/resource counts and uptime.

Both transports share a single pipeline:

Agent (MCP)  → POST /mcp              → auth → validate → middleware → handler
Browser      → GET /lookup-user?id=123 → auth → validate → middleware → handler

One handler. Two protocols. Same auth, same validation, same middleware.

Get started

New app

npx @schrepa/create-graft-app my-app
cd my-app
npm install
npm run dev

Open the studio to browse and test your tools: npm run studio

Wrap an existing API

If you have an OpenAPI spec:

npx @schrepa/graft serve --openapi ./openapi.yaml --target http://localhost:8000

Or create a graft.proxy.yaml to hand-pick the endpoints you want to expose:

target: http://localhost:8000
tools:
  - method: GET
    path: /items
    name: list_items
    description: List items with optional filters
    parameters:
      type: object
      properties:
        q: { type: string, description: Search query }
        status: { type: string, enum: [draft, active, archived] }
  - method: POST
    path: /entries
    name: create_entry
    description: Create a new entry
    parameters:
      type: object
      properties:
        title: { type: string }
        tags: { type: array, items: { type: string } }
      required: [title]
npx @schrepa/graft serve

Zero code changes. Any language. Any framework.

Add to an existing app

Use .toFetch() for fetch-based runtimes or .toNodeHandler() for Node servers:

// Bun / Deno / Cloudflare Workers
export default { fetch: app.toFetch() }

// Node.js with your own http server
const handler = app.toNodeHandler()
http.createServer(handler).listen(3000)

Tools

Tools are the core building block. Each tool becomes both an MCP tool and an HTTP endpoint:

app.tool('list_items', {
  description: 'List items with optional filters',
  params: z.object({
    q: z.string().optional(),
    status: z.enum(['draft', 'active', 'archived']).optional(),
  }),
  handler: ({ q, status }) => {
    // Return any JSON-serializable value
    return items.filter((item) => /* ... */)
  },
})
  • name — Stable identifier agents depend on. Tool names map to HTTP paths: list_items becomes GET /list-items.
  • description — Agents read this to decide when to call your tool.
  • params — Zod schema. Validated before your handler runs. Advertised in MCP tools/list.
  • handler(params, ctx) — Receives validated params and a ToolContext with logging and progress reporting.
  • sideEffects — Set true for mutations. Changes the HTTP method from GET to POST.
  • output — Optional Zod schema advertised as outputSchema in MCP.
  • auth — See Authentication.
  • expose — Control visibility: 'both' (default), 'mcp' (MCP only, no HTTP), 'http' (HTTP only, hidden from MCP tools/list).
  • http — { method, path } to customize the HTTP route.
// MCP-only tool (no HTTP endpoint)
app.tool('internal_task', { description: '...', expose: 'mcp', handler: () => {} })

// Custom HTTP route
app.tool('search', {
  description: '...',
  http: { method: 'POST', path: '/api/search' },
  handler: () => {},
})

For larger apps, define tools in modules and register them by passing the defined tool object:

// src/tools/list-items.ts
import { defineTool, z } from '@schrepa/graft'

export const listItemsTool = defineTool('list_items', {
  description: 'List items with optional filters',
  params: z.object({
    q: z.string().optional(),
  }),
  handler: ({ q }) => listItems(q),
})

// src/app.ts
import { createApp } from '@schrepa/graft'
import { listItemsTool } from './tools/list-items.js'

const app = createApp({ name: 'my-app' })
app.tool(listItemsTool)

Resources

Resources expose read-only data to agents.

  • auth works on both static resources and resource templates.
  • HTTP resource routes run through the same dispatch pipeline as tools.
  • MCP resources/read uses that same pipeline, so auth, middleware, lifecycle hooks, and telemetry stay consistent.
app.resource({
  uri: 'config://settings',
  name: 'App Settings',
  description: 'Current application settings',
  mimeType: 'application/json',
  auth: true,
  handler: () => getSettings(),
})

Resources auto-generate HTTP GET endpoints (URI config://settings becomes GET /settings). Set expose: 'mcp' to make them MCP-only.

Prompts

Prompts are reusable message templates for agents:

app.prompt({
  name: 'summarize',
  description: 'Summarize content with optional constraints',
  params: z.object({
    style: z.string().optional().describe('Summary style (e.g. brief, detailed)'),
  }),
  handler: ({ style }) => [
    { role: 'user', content: `Summarize the following content.${style ? ` Use a ${style} style.` : ''}` },
  ],
})

Authentication

Protect tools that require user identity:

import { createApp, AuthError } from '@schrepa/graft'

const app = createApp({
  name: 'my-app',
  authenticate: (request) => {
    const token = request.headers.get('authorization')
    if (!token) throw new AuthError('Unauthorized', 401)
    const user = verifyToken(token)
    return { subject: user.id, roles: user.roles }
  },
})

// Auth required — authenticate() must return successfully
app.tool('create_entry', { auth: true, /* ... */ })

// Auth with role check
app.tool('delete_user', { auth: ['admin'], /* ... */ })

// Explicit object form also works
app.tool('audit_log', { auth: { roles: ['auditor'] }, /* ... */ })

// No auth — anyone can call this, authenticate() is skipped entirely
app.tool('list_items', { /* ... */ })

Auth is only enforced for tools that declare it. Tools without auth skip authentication entirely.

Middleware

Add cross-cutting logic that wraps every tool call:

const app = createApp({
  name: 'my-app',
  // Global middleware via options
  onToolCall: async (ctx, next) => {
    const start = Date.now()
    const result = await next()
    console.log(`${ctx.meta.toolName} took ${Date.now() - start}ms`)
    return result
  },
})

// Or add middleware with .use() — runs in registration order
app.use(async (ctx, next) => {
  console.log(`calling ${ctx.meta.toolName}`)
  return next()
})

Middleware runs for both MCP and HTTP calls through the same pipeline.

HTTP routes

Register non-tool HTTP endpoints:

app.route('GET', '/ping', () => ({ status: 'ok' }))
app.route('POST', '/webhooks/stripe', async (request) => {
  const body = await request.json()
  // handle webhook
  return new Response('ok')
})

These are plain HTTP routes — not MCP tools, not visible to agents.

Deployment

Node.js

// src/app.ts
export default app
graft serve -e src/app.ts --port 3000

Or use .serve() directly:

app.serve({ port: 3000 })

Bun, Deno, Cloudflare Workers

// Bun
export default { fetch: app.toFetch() }

// Deno
Deno.serve(app.toFetch())

// Cloudflare Workers
export default { fetch: app.toFetch() }

Frontend + Backend on different origins

Set apiUrl so discovery documents point to the real backend regardless of which host serves them:

const app = createApp({
  name: 'my-api',
  apiUrl: process.env.API_URL ?? 'http://localhost:3000',
})

Then proxy /.well-known/* from your frontend to the backend. Next.js example:

// next.config.ts
async rewrites() {
  return [{ source: '/.well-known/:path*', destination: 'http://localhost:3000/.well-known/:path*' }]
}

Lifecycle hooks

const app = createApp({
  name: 'my-app',
  onStart: () => console.log('Server starting'),
  onShutdown: () => db.close(),
})

Auto-served docs and discovery

Every Graft server auto-serves these framework endpoints alongside your tool and resource routes:

Endpoint Description
/.well-known/agent.json Agent discovery — tools, resources, and MCP endpoint
/.well-known/mcp.json MCP server card — protocol version, capabilities, transport URL
/openapi.json Auto-generated OpenAPI 3.1 spec for all HTTP tool endpoints
/docs Interactive API reference UI (Scalar)
/llms.txt Compact tool listing for LLMs
/llms-full.txt Detailed tool listing with parameters, examples, and auth info
/health Health check — status, tool/resource/prompt counts, uptime

Disable or customize any endpoint:

const app = createApp({
  name: 'my-app',
  discovery: {
    docs: false,           // disable /docs
    llmsTxt: './llms.txt', // serve from static file
  },
  healthCheck: { path: '/api/health' }, // customize health path
})

Connect to Claude Desktop

The quickest way:

npx @schrepa/graft install -e src/app.ts --stdio

This writes the config automatically. Or add it manually:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Stdio transport (Claude launches your app):

{
  "mcpServers": {
    "my-app": {
      "command": "npx",
      "args": ["@schrepa/graft", "serve", "--stdio", "-e", "src/app.ts"]
    }
  }
}

HTTP transport (your server must be running):

{
  "mcpServers": {
    "my-app": {
      "url": "http://localhost:3000/mcp"
    }
  }
}

CLI

Command Description
graft serve Start the server (--stdio for MCP stdio transport)
graft dev Start dev server with auto-restart on file changes
graft check Validate tool definitions without starting a server
graft test Run tool examples as smoke tests (source apps only)
graft studio Open the visual tool explorer UI
graft install Add your server to Claude Desktop config
graft add-tool <name> Generate a new tool file with scaffold
# Source app
graft serve -e src/app.ts               # HTTP server on :3000
graft dev -e src/app.ts                  # dev server with auto-restart
graft serve -e src/app.ts --stdio        # stdio transport (for Claude Desktop)
graft check -e src/app.ts               # validate tool definitions
graft test -e src/app.ts                 # run example smoke tests
graft test -e src/app.ts -t echo          # test a single tool
graft studio -e src/app.ts              # open visual studio UI
graft install -e src/app.ts --stdio     # add to Claude Desktop config
graft add-tool search_docs              # scaffold a new tool file

# Proxy (OpenAPI or config file)
graft serve --openapi ./spec.yaml --target http://localhost:8000
graft dev --openapi ./spec.yaml --target http://localhost:8000
graft check --openapi ./spec.yaml
graft studio --openapi ./spec.yaml --target http://localhost:8000

# Studio with a running server
graft studio --url http://localhost:3000/mcp

Options: --port <port>, --header k=v (repeatable), --locked-header k=v (repeatable, cannot be overridden by callers).

Testing

Define examples on your tools and Graft runs them as smoke tests:

app.tool('echo', {
  description: 'Echo a message back to the caller',
  params: z.object({ message: z.string() }),
  examples: [
    { name: 'hello', args: { message: 'hello' }, result: { message: 'hello' } },
  ],
  handler: ({ message }) => ({ message }),
})
graft test -e src/app.ts

Each example is dispatched through the full pipeline (auth, validation, middleware, handler) and the result is compared using deep partial matching — your expected result only needs to be a subset of the actual output.

Testing is available for source-based apps (-e flag). Use -t <name> to test a single tool.

Packages

Package Description
@schrepa/graft CLI, createApp(), and proxy mode
@schrepa/create-graft-app Project scaffolding

Development

pnpm install
pnpm build
pnpm test

License

Apache-2.0

推荐服务器

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 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

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

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

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

官方
精选
Python
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 模型以安全和受控的方式获取实时的网络信息。

官方
精选