Expose Backend Services as Agent Tools Through One Server
You have several gRPC services -- graph, vector, pathway, trace --
and you need agents to reach them as tools. Writing a separate MCP
wrapper for each service means duplicating schema translation,
session management, and authentication logic. The MCP service reads
a single tool configuration from config/config.json,
creates gRPC clients for each backend, and exposes every configured
endpoint as a typed MCP tool through one HTTP/SSE server.
This guide walks through understanding the configuration, starting the MCP service, connecting a client, and verifying that backend RPCs are reachable as agent tools.
Prerequisites
- Node.js 18+
-
Generated client code available (run
npx fit-codegen --allif not) -
All backend services running (
npx fit-rc startorjust guide) -
The
MCP_TOKENenvironment variable set (the MCP service requires a bearer token for authentication)
Install the MCP SDK if you are building a client:
npm install @modelcontextprotocol/sdk
Architecture overview
The MCP service is the only non-gRPC service in the stack. It
exposes an HTTP/SSE interface using
@modelcontextprotocol/sdk and delegates every tool call
to one of the gRPC backend services. Each client session gets its
own McpServer instance for isolation.
Agent SDK ──── HTTP/SSE ──── MCP service ──┬── gRPC ── graph
├── gRPC ── vector
├── gRPC ── pathway
└── resource index
Tool registration is declarative. The
service.mcp.tools section of
config/config.json maps tool names to gRPC methods:
{
"service": {
"mcp": {
"tools": {
"QueryByPattern": {
"method": "graph.Graph.QueryByPattern",
"description": "Retrieves structured data by traversing graph relationships using triple patterns."
},
"SearchContent": {
"method": "vector.Vector.SearchContent",
"description": "Find detailed content using semantic similarity search."
},
"DescribeJob": {
"method": "pathway.Pathway.DescribeJob",
"description": "Describe a job at (discipline, level, optional track)."
}
}
}
}
}
Each entry specifies a method in
<package>.<Service>.<RPC> format and
a description that becomes the tool's
human-readable summary. The MCP service reads the codegen metadata
for each method to build a Zod schema for parameter validation, so
tool parameters are typed automatically from the proto definitions.
Default tool set
The MCP service ships with these tools pre-configured:
| Tool | Backend | Purpose |
|---|---|---|
GetOntology |
graph | Returns entity types and relationship predicates |
GetSubjects |
graph | Lists entity URIs, optionally filtered by type |
QueryByPattern |
graph | Traverses relationships using triple patterns |
SearchContent |
vector | Semantic similarity search over indexed content |
ListJobs |
pathway | Lists valid discipline/level/track combinations |
DescribeJob |
pathway | Derives a full role definition |
ListAgentProfiles |
pathway | Lists valid discipline/track combinations |
DescribeAgentProfile |
pathway | Derives an agent profile |
DescribeProgression |
pathway | Computes the delta between two levels |
ListJobSoftware |
pathway | Derives the software toolkit for a role |
Start the MCP service
The MCP service starts as part of the service stack:
npx fit-rc start
Or start it individually during development:
MCP_TOKEN=your-token node --watch services/mcp/server.js
The server listens on port 3005 by default (configurable in
config/config.json). Verify it is running:
curl http://localhost:3005/health
Expected output:
{"status":"ok"}
Connect a client
Agents connect to the MCP service over HTTP. The
@modelcontextprotocol/sdk provides the client
transport:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(
new URL("http://localhost:3005"),
{
requestInit: {
headers: {
Authorization: `Bearer ${process.env.MCP_TOKEN}`,
},
},
},
);
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
List available tools
const tools = await client.listTools();
console.log("Available tools:", tools.tools.length);
for (const tool of tools.tools) {
console.log(` ${tool.name}: ${tool.description}`);
}
Expected output:
Available tools: 10
GetOntology: Returns all entity types and relationship predicates in the knowledge graph.
GetSubjects: Lists entity URIs in the graph, optionally filtered by type.
QueryByPattern: Retrieves structured data by traversing graph relationships using triple patterns.
SearchContent: Find detailed content using semantic similarity search.
ListJobs: List jobs (discipline x level x track) defined in the pathway standard.
DescribeJob: Describe a job at (discipline, level, optional track) including skills, behaviours, and responsibilities.
ListAgentProfiles: List static agent profile (discipline, track) combinations.
DescribeAgentProfile: Describe stage agent profiles for a (discipline, track).
DescribeProgression: Compute the progression delta between two levels of the same discipline.
ListJobSoftware: List the software toolkit derived for a job.
Call a tool
const result = await client.callTool({
name: "DescribeJob",
arguments: {
discipline: "software_engineering",
level: "J070",
track: "platform",
},
});
console.log(result.content[0].text.substring(0, 300));
The response is the Turtle RDF content from the pathway service, wrapped in the MCP content format.
How tool registration works
When the MCP service starts,
registerToolsFromConfig iterates the tool configuration
and for each entry:
-
Parses the
methodstring into package, service, and RPC name. - Looks up the codegen metadata for the RPC's request type fields.
- Builds a Zod schema from the field metadata for parameter validation.
-
Registers the tool on the
McpServerwith the schema and a handler that normalizes parameters, creates a typed request viafromObject, calls the gRPC client, and returns the result.
When the RPC returns resource identifiers (rather than content), the service resolves them through the resource index and returns the content as text.
Session management
Each client connection gets its own McpServer and
transport pair. Sessions are tracked by session ID and reaped after
30 minutes of inactivity. The reaping interval runs every 60
seconds.
When a client disconnects, its session is removed from the session map. When the server shuts down, it closes all active sessions before stopping the HTTP listener.
Authentication
Every request must include a Bearer token in the
Authorization header. The token is compared against
config.mcpToken(), which reads from the
MCP_TOKEN environment variable. Requests without a
valid token receive a 401 response.
Verify
You have reached the outcome of this guide when:
-
The MCP service starts and
/healthreturns{"status":"ok"}. - An MCP client connects with the correct bearer token.
-
listToolsreturns all configured tools with descriptions. -
callToolfor a pathway RPC returns Turtle RDF content. -
callToolfor a graph or vector RPC returns content or resolved resource text.
If the MCP service starts but tool calls fail, confirm the backend
gRPC services are running with npx fit-rc status. The
MCP service cannot serve tool calls if the backend it delegates to
is unreachable.
What's next
- Add a Service to the MCP Surface -- register a new backend service as agent tools without writing integration code.
- Ground Agents in Context -- the graph and vector services that the MCP service delegates to for knowledge queries.
- Query the Engineering Standard -- the pathway service that the MCP service delegates to for derivation queries.