How to Build an AI Agent from Your Existing REST API — Without an MCP Server
You already have a REST API. You don't want to rewrite it, run a dedicated protocol server, or learn JSON-RPC just to let an AI agent call GET /orders or POST /users.
RCP (REST Connector Protocol) is the shortest path: add one GET /manifest route that describes your endpoints as tools. Your AI agent fetches it and calls your API directly — stateless HTTP, no persistent connection.
RCP is new — ChatGPT/Claude don't speak it natively. Your AI agent does. It fetches the manifest and exposes the tools to the model via the OpenAI SDK, LangChain, or Gemini. That's the whole trick.
- Docs: https://rcp.hasanraiyan.me
- GitHub: https://github.com/hasanraiyan/rcp
-
npm:
rcp-sdk— https://www.npmjs.com/package/rcp-sdk
Why not just use MCP?
MCP is great when you need rich, stateful capabilities — resources, prompts, elicitation, sampling, a long-lived session. That power costs a protocol server + JSON-RPC + transport negotiation.
RCP is for the far more common case:
"I have a REST API. I want an AI agent to call some of its endpoints as tools, and I don't want to run anything besides my existing API plus one more route that returns JSON."
If you outgrow that, reach for MCP. That's a real signal, not a gap RCP tries to fill.
Full comparison: https://rcp.hasanraiyan.me/docs/vs-mcp
| RCP | MCP | |
|---|---|---|
| Connection | Stateless HTTP | Persistent session |
| Server | Your existing REST API (or static JSON) | Dedicated protocol server |
| Transport | Plain HTTP | JSON-RPC over stdio / Streamable HTTP |
| Best fit | You already have endpoints to expose | Rich, stateful capabilities |
How it works in 3 steps
- Discovery:
GET /manifest→{ rcpVersion, auth, tools[] } - Selection: Agent sees only
exposedParams(resolver-boundtenantId/userIdalready stripped) - Execution: Agent picks a tool → client fills resolvers from trusted
ctx→ calls the tool's own URL directly
Client Server
GET /manifest ─────────────► 200 { rcpVersion: "0.1", tools: [...] }
...agent picks create_order...
POST https://api.acme.dev/orders ──► { id: "ord_193" }
5-minute walkthrough
1. Install
npm i rcp-sdk
# also: pnpm add rcp-sdk / yarn add rcp-sdk
rcp-sdk ships two entry points: rcp-sdk/client (your AI agent) and rcp-sdk/server (defineTool() helper).
2. Define a tool on your server
defineTool() never touches the network — it returns a plain object. You serve it.
// server.ts
import { defineTool } from 'rcp-sdk/server';
import { z } from 'zod';
export const getWeather = defineTool({
name: 'get_weather',
description: 'Get current weather for a city.',
method: 'GET',
args: z.object({
city: z.string().describe('City name, e.g. "Paris"'),
}),
url: 'https://internal.example.com/weather',
queryParams: { city: (t) => t.arg('city') },
responseMappings: {
temperatureC: '@temperatureC',
conditions: '@conditions',
},
});
zod → params mapping is automatic: z.string().describe() becomes { name, type: 'string', description, required: true }. Typo in t.arg() is a compile error, not a silent bug.
3. Serve the manifest
One route. A plain Node http server is fully conformant — no framework required.
import { createServer } from 'node:http';
const manifest = {
rcpVersion: '0.1',
auth: { type: 'header', header: 'Authorization', scheme: 'Bearer' },
tools: [getWeather],
};
createServer((req, res) => {
if (req.url === '/manifest') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(manifest));
return;
}
// ... your /weather handler
}).listen(4310);
A static JSON file behind a CDN is also a valid RCP server — curl can test it.
4. Build an AI agent that uses it
Option A — OpenAI SDK (ChatGPT / GPT-4o via your agent)
import { createRcpClient } from 'rcp-sdk/client';
import { rcpToolsToOpenAiTools } from 'rcp-sdk/adapters/openai';
import OpenAI from 'openai';
const rcp = createRcpClient({ auth: { type: 'header', secret: process.env.SERVER_TOKEN! } });
const { tools } = await rcp.discover('http://localhost:4310/manifest');
const openaiTools = rcpToolsToOpenAiTools(tools); // ChatCompletionTool[]
const openai = new OpenAI();
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'What is the weather in Paris?' }],
tools: openaiTools,
});
// model returns tool_call → you call rcp.call(tool, args) → feed result back → final answer
Your agent speaks RCP — ChatGPT doesn't need to. Full adapter docs: https://rcp.hasanraiyan.me/docs/sdk/openai
Option B — LangChain / LangGraph
import { createRcpClient } from 'rcp-sdk/client';
import { loadRcpLangChainTools } from 'rcp-sdk/adapters/langchain';
import { ChatOpenAI } from '@langchain/openai';
import { createAgent } from 'langchain';
const rcp = createRcpClient();
const tools = await loadRcpLangChainTools('https://api.example.com/manifest', rcp, {
context: { userId: 'u_42' }, // resolver-bound, never reaches the model
serverName: 'myApi',
});
const model = new ChatOpenAI({ model: 'gpt-4o-mini' });
const agent = createAgent({ model, tools });
Supports MultiServerRcpClient for multiple manifests. Docs: https://rcp.hasanraiyan.me/docs/sdk/langchain
Option C — Gemini (Google GenAI)
import { rcpToolsToGeminiInteractionsTools } from 'rcp-sdk/adapters/gemini';
import { GoogleGenAI } from '@google/genai';
const geminiTools = rcpToolsToGeminiInteractionsTools(tools);
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
const interaction = await ai.interactions.create({
model: 'models/gemini-2.5-flash',
input: 'What is the weather in Paris?',
tools: geminiTools,
});
Both Interactions API (flat) and classic generateContent supported: https://rcp.hasanraiyan.me/docs/sdk/gemini
The killer feature: params the model never sees
A REST endpoint often needs tenantId or userId — but you don't want the LLM to guess or spoof it.
Resolvers are client-side only — nothing declared in the manifest.
const client = createRcpClient({
resolvers: {
tenantId: (ctx) => ctx.tenantId, // hidden from model
userId: (ctx) => ctx.currentUserId,
},
});
- At
discover()time:tenantIdis removed fromexposedParams— the model has no field to fill. - At
call()time: client fills the real value from trustedctxbefore the HTTP request leaves. - No value to resolve → call fails before any HTTP goes out.
MCP has no equivalent — inputSchema is plain JSON Schema, every property is model-fillable. Details: https://rcp.hasanraiyan.me/docs/concepts/resolvers
Full runnable example — Express + AI agent
Two standalone projects that install rcp-sdk from npm (not from this repo):
git clone https://github.com/hasanraiyan/rcp.git && cd rcp/examples
cd express && npm install && npm start & # your REST API + one /rcp/manifest route
cd ../openai-client && npm install # the AI agent side
cp .env.example .env # add your OPENAI_API_KEY
npm start -- "List my tasks, then mark the first incomplete one as done."
examples/express has no OpenAI code. examples/openai-client has no Express code. See https://rcp.hasanraiyan.me/docs/examples
Minimal no-framework demo also lives in typescript/examples/basic — one http server, one client, pnpm example and you see Discovered 1 tool(s).
When to use RCP vs MCP
Use RCP when:
- You already have a REST API and want an AI agent to call a handful of endpoints
- Every call is one-shot: fetch, call, get response
- You need tenant/user isolation via resolvers
- You want any client to integrate by reading JSON, no SDK required on server side
Use MCP when:
- Model needs to browse resources as a distinct concept from tools
- You need prompt templates, elicitation, or sampling
- Long-lived stateful session is valuable
Decision checklist: https://rcp.hasanraiyan.me/docs/vs-mcp
Full spec v0.1: https://rcp.hasanraiyan.me/docs/spec
Try it
npm i rcp-sdk
Then defineTool() → serve manifest → createRcpClient().discover() → adapter → agent. 5 minutes, no protocol server.
Star the repo if this is useful — it helps the protocol get found: https://github.com/hasanraiyan/rcp
Canonical: https://rcp.hasanraiyan.me/docs/getting-started — originally published on rcp.hasanraiyan.me
Top comments (0)