Graft
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Graftcreate a tool to look up users by ID"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 appThat 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 → handlerOne 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 devOpen 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:8000Or 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 serveZero 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_itemsbecomesGET /list-items.description— Agents read this to decide when to call your tool.params— Zod schema. Validated before your handler runs. Advertised in MCPtools/list.handler(params, ctx)— Receives validated params and aToolContextwith logging and progress reporting.sideEffects— Settruefor mutations. Changes the HTTP method from GET to POST.output— Optional Zod schema advertised asoutputSchemain 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.
authworks on both static resources and resource templates.HTTP resource routes run through the same dispatch pipeline as tools.
MCP
resources/readuses 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 appgraft serve -e src/app.ts --port 3000Or 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 |
| Agent discovery — tools, resources, and MCP endpoint |
| MCP server card — protocol version, capabilities, transport URL |
| Auto-generated OpenAPI 3.1 spec for all HTTP tool endpoints |
| Interactive API reference UI (Scalar) |
| Compact tool listing for LLMs |
| Detailed tool listing with parameters, examples, and auth info |
| 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 --stdioThis writes the config automatically. Or add it manually:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%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 |
| Start the server ( |
| Start dev server with auto-restart on file changes |
| Validate tool definitions without starting a server |
| Run tool examples as smoke tests (source apps only) |
| Open the visual tool explorer UI |
| Add your server to Claude Desktop config |
| 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/mcpOptions: --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.tsEach 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 |
CLI, | |
Project scaffolding |
Development
pnpm install
pnpm build
pnpm testLicense
Apache-2.0
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/schrepa/graft'
If you have feedback or need assistance with the MCP directory API, please join our Discord server