MCP as External API
Ship your MCP server publicly. Users connect Claude, ChatGPT, Cursor. "Use us where you already work."
Summary
Your MCP server that powers your in-app agent can become a distribution channel. Publish it with OAuth 2.1 + DPoP authentication so external AI clients (Claude Desktop, ChatGPT, Cursor) can connect and use your agent's tools. Turns your agent into a service.
- MCP-compatible: Standard protocol; any capable client can implement.
- OAuth 2.1 + DPoP: User-initiated auth; tokens tied to device (Demonstration of Proof-of-Possession prevents token theft).
- No API keys: Users authorize once; client stores refresh token.
- Same tools: Your in-app agent and external clients use identical tool definitions.
- Distribution: Users find you in Claude's model directory, ChatGPT's app store, Cursor's integrations.
Why Ship MCP Publicly?
- Distribution: Users discover you without visiting your website.
- Usage in flow: Users don't leave their editor or AI client to manage your product.
- Network effect: Each AI client's user base becomes your potential customers.
- Low friction: "Add MCP server" is 3 clicks; "create account" is 5 steps.
Server Setup
Your MCP server already exists (powers your in-app agent). Make it available on a stable URL:
// mcp/server.ts — your existing server
import { Server } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server({
name: "my-agent-server",
version: "1.0.0",
});
// Register all tools (same as in-app)
registerCustomerTools(server, ctx);
registerOrderTools(server, ctx);
registerInvoiceTools(server, ctx);
// ...
// Expose both stdio (for local development) and HTTP (for external clients)
export async function startMcpServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
export async function getMcpServer() {
return server;
}HTTP Transport
Wrap your server in an HTTP server so external clients can reach it:
// mcp/http-transport.ts
import express from "express";
import { getMcpServer } from "./server";
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/shared/jsonrpc.js";
const app = express();
app.use(express.json());
const mcpServer = await getMcpServer();
// POST /mcp/jsonrpc — accepts JSON-RPC 2.0 requests
app.post("/mcp/jsonrpc", async (req, res) => {
const message: JSONRPCMessage = req.body;
try {
const result = await mcpServer.handleMessage(message);
res.json(result);
} catch (err) {
res.status(400).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: err instanceof Error ? err.message : "Internal error",
},
id: message.id,
});
}
});
// GET /.well-known/mcp/server-card.json — MCP server metadata
app.get("/.well-known/mcp/server-card.json", (req, res) => {
res.json({
name: "my-agent-server",
description: "Access my SaaS tools from Claude, ChatGPT, Cursor",
version: "1.0.0",
transport: {
type: "streamable-http",
url: "https://api.example.com/mcp"
},
auth: {
type: "oauth2",
authorizationUrl: "https://api.example.com/oauth/authorize",
tokenUrl: "https://api.example.com/oauth/token",
scopes: ["api:all"],
},
});
});
app.listen(3000, () => console.log("MCP HTTP server listening on port 3000"));OAuth 2.1 + DPoP
Implement OAuth 2.1 with DPoP for secure external auth:
// auth/oauth.ts
import { generateCodeChallenge, generateState } from "oauth-pkce";
import jwt from "jsonwebtoken";
import { createDPoPProof } from "oauth-dop";
// Step 1: Generate authorization URL (user clicks "Connect")
export function getAuthorizationUrl(clientId: string, redirectUri: string): string {
const state = generateState();
const challenge = generateCodeChallenge();
// Store challenge in session (server-side)
sessionStore.set(`pkce:${state}`, challenge);
return new URL("https://api.example.com/oauth/authorize", {
client_id: clientId,
redirect_uri: redirectUri,
response_type: "code",
scope: "api:all",
state,
code_challenge: challenge.challenge,
code_challenge_method: "S256",
}).toString();
}
// Step 2: Exchange code for token (OAuth callback)
app.get("/oauth/callback", async (req, res) => {
const { code, state } = req.query as Record<string, string>;
// Verify state
const storedChallenge = sessionStore.get(`pkce:${state}`);
if (!storedChallenge) {
return res.status(400).json({ error: "Invalid state" });
}
// Exchange code for access token (server-side; safe)
const token = await exchangeCode(code, {
clientId: process.env.OAUTH_CLIENT_ID,
clientSecret: process.env.OAUTH_CLIENT_SECRET,
codeVerifier: storedChallenge.verifier,
});
// Return token to client (user stores in their MCP config)
res.json({
access_token: token.access_token,
refresh_token: token.refresh_token,
expires_in: token.expires_in,
});
});
// Step 3: MCP client uses token with DPoP proof
// Client does this automatically when calling MCP methods
export function createDPoPHeader(method: string, url: string, accessToken: string): string {
const proof = createDPoPProof({
method,
url,
nonce: generateNonce(), // Server sends in 401 DPoP-Nonce header
});
return `DPoP ${accessToken}`;
}
// Step 4: Verify token + DPoP proof on MCP requests
app.post("/mcp/jsonrpc", async (req, res, next) => {
const authHeader = req.header("Authorization");
const dPoPProof = req.header("DPoP");
if (!authHeader || !dPoPProof) {
return res.status(401).json({ error: "Missing auth headers" });
}
const [scheme, token] = authHeader.split(" ");
if (scheme !== "DPoP") {
return res.status(401).json({ error: "Invalid auth scheme" });
}
try {
// Verify token
const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY);
// Verify DPoP proof
const proof = jwt.verify(dPoPProof, process.env.DPOP_PUBLIC_KEY);
if (proof.htm !== req.method || !proof.htu.startsWith(req.url)) {
throw new Error("DPoP proof invalid");
}
// Attach user context
req.userId = payload.sub;
req.teamId = payload.team_id;
next();
} catch (err) {
return res.status(401).json({ error: "Unauthorized" });
}
});Tool Filtering by User
Each user's access to tools may differ based on their subscription or permissions:
// mcp/server.ts — modified to support external clients
export async function getMcpServer(userId?: string, teamId?: string) {
const server = new Server({ name: "my-agent-server" });
// Load user's scopes/permissions
const scopes = userId && teamId
? await getUserScopes(userId, teamId)
: ["api:read"]; // Anonymous clients get read-only access
// Register only tools they have access to
if (scopes.includes("customers:read") || scopes.includes("api:all")) {
registerCustomerTools(server, { userId, teamId, scopes });
}
if (scopes.includes("orders:write") || scopes.includes("api:all")) {
registerOrderTools(server, { userId, teamId, scopes });
}
// ...
return server;
}
// Modify the HTTP handler to pass user context
app.post("/mcp/jsonrpc", async (req, res) => {
const userId = (req as any).userId; // From OAuth middleware
const teamId = (req as any).teamId;
const message = req.body;
const mcpServer = await getMcpServer(userId, teamId);
const result = await mcpServer.handleMessage(message);
res.json(result);
});Client Configuration
Users add your MCP server to their Claude Desktop or ChatGPT configuration:
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"my-agent": {
"command": "npx",
"args": ["@my-org/mcp-server"],
"env": {
"MCP_SERVER_URL": "https://api.example.com/mcp",
"OAUTH_CLIENT_ID": "your-client-id",
"OAUTH_REDIRECT_URI": "http://localhost:3000/oauth/callback"
}
}
}
}ChatGPT Custom GPT
- User creates a Custom GPT.
- Adds "Actions" → "Authentication" → OAuth 2.1.
- Provides authorization URL, token URL, scopes.
- Adds action schema (points to your
/mcp/openapiendpoint).
OpenAPI Bridge (Optional)
For ChatGPT compatibility, expose tool descriptions in OpenAPI format:
// mcp/openapi.ts
app.get("/mcp/openapi.json", async (req, res) => {
const userId = (req as any).userId;
const teamId = (req as any).teamId;
const mcpServer = await getMcpServer(userId, teamId);
const tools = mcpServer.listTools();
const spec = {
openapi: "3.1.0",
info: {
title: "My Agent API",
description: "Access your SaaS tools from Claude, ChatGPT, Cursor",
version: "1.0.0",
},
servers: [{ url: "https://api.example.com/mcp" }],
paths: tools.reduce((acc, tool) => {
acc[`/tools/${tool.name}`] = {
post: {
summary: tool.title,
description: tool.description,
requestBody: {
content: {
"application/json": {
schema: tool.inputSchema,
},
},
},
responses: {
"200": {
description: "Success",
content: {
"application/json": {
schema: tool.outputSchema,
},
},
},
},
},
};
return acc;
}, {} as Record<string, any>),
};
res.json(spec);
});Publishing & Discovery
- MCP Registry: Publish a registry entry when your server is public and stable.
- App directories: Use host-specific app or integration directories where they exist.
- Docs: Add connection instructions for Claude, ChatGPT, Cursor, and other MCP clients.
- Website: Add "Connect with MCP" buttons that point to the server card and setup docs.
Monitoring & Rate Limiting
Track external usage separately from in-app usage:
// middleware/rate-limit.ts
import { RateLimiter } from "rate-limiter";
const limiter = new RateLimiter({
redis,
keyPrefix: "mcp:",
});
app.post("/mcp/jsonrpc", async (req, res, next) => {
const userId = (req as any).userId;
const limit = await limiter.check(userId, {
max: 1000, // Requests per day
window: 24 * 60 * 60, // 24 hours
});
if (!limit.ok) {
return res.status(429).json({
error: "Rate limit exceeded",
retryAfter: limit.resetAt,
});
}
next();
});
// Log all external MCP calls
app.post("/mcp/jsonrpc", (req, res, next) => {
const userId = (req as any).userId;
const message = req.body;
logger.info("[MCP External] Tool call", {
userId,
method: message.method,
tool: message.params?.name,
source: "external",
});
next();
});Checklist
- Expose your MCP server on a public HTTPS URL.
- Implement OAuth 2.1 + DPoP for secure external auth.
- Add
/.well-known/mcp/server-card.jsondiscovery metadata. - Modify tool registration to respect user scopes.
- Document setup for Claude Desktop, ChatGPT, Cursor.
- Add rate limiting for external requests.
- Monitor external tool calls separately from in-app.
- Create "Use with Claude" / "Use with ChatGPT" buttons on docs.
- Test with actual Claude Desktop, ChatGPT client.
See Also
- Tool Design — tool schema and descriptions.
- Tool Annotations — declaring tool intent.
- MCP Servers — deploying and managing MCP servers.