DEV Community

Cover image for How to Build an AI Agent from Your Existing REST API — Without an MCP Server
Raiyan Hasan
Raiyan Hasan

Posted on Originally published at rcp.hasanraiyan.me

How to Build an AI Agent from Your Existing REST API — Without an MCP Server

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.


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

  1. Discovery: GET /manifest{ rcpVersion, auth, tools[] }
  2. Selection: Agent sees only exposedParams (resolver-bound tenantId/userId already stripped)
  3. 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" }
Enter fullscreen mode Exit fullscreen mode

5-minute walkthrough

1. Install

npm i rcp-sdk
# also: pnpm add rcp-sdk / yarn add rcp-sdk
Enter fullscreen mode Exit fullscreen mode

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',
  },
});
Enter fullscreen mode Exit fullscreen mode

zodparams 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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode
  • At discover() time: tenantId is removed from exposedParams — the model has no field to fill.
  • At call() time: client fills the real value from trusted ctx before 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."
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)