@codeflow-team/mcp
v0.3.0
Published
Turn MCP tool schemas into CodeFlow ToolDefinitions — safe name slugging, cursor paging, inline $ref resolution. Zero runtime dependency on the MCP SDK.
Maintainers
Readme
@codeflow-team/mcp
The optional MCP adapter: it turns what an MCP server reports from tools/list into ToolDefinitions that @codeflow-team/core can register, generate types for, and project into nodes.
Once a tool is in the registry, nothing downstream can tell it came from MCP rather than from a local function or a REST SDK. That is the whole point of the adapter.
See the root README for what CodeFlow is.
Install
Prepared for npm as v0.1.0; until the first release lands, use the workspace copy:
"dependencies": { "@codeflow-team/mcp": "workspace:*" }Zero runtime dependencies beyond core, which it uses for types only. @modelcontextprotocol/sdk is an optional peer: nothing here opens a connection or owns a transport. An SDK Client is accepted, and so is any object with a listTools().
The four entry points
registerMcpServer(registry, client, options)
The one-liner. Discovers every tool the server has (following nextCursor until it runs out) and registers them all.
import { registerMcpServer } from "@codeflow-team/mcp";
await registerMcpServer(registry, client, { namespace: "github" });
// → registry now holds github.getIssue, github.listPRs, …
// and `codeflow generate` emits them into generated/tools.d.ts.discoverMcpTools(client, options) / registerMcpTools(registry, definitions)
The same thing in two halves, so you can inspect, filter or persist the definitions before they reach the registry. That split is how @codeflow-team/examples froze 65 real schemas into the repository — the examples run offline against captured schemas rather than against live servers.
mcpToolsToDefinitions(tools, options)
Pure mapping, no client involved. Feed it the raw tools array:
import { createRegistry, generateToolsDts } from "@codeflow-team/core";
import { mcpToolsToDefinitions, registerMcpTools } from "@codeflow-team/mcp";
// Exactly what an MCP server returns from `tools/list`.
const listed = [
{
name: "read_text_file",
description: "Read a file from disk. Supports '**/*.md' globs.",
inputSchema: {
type: "object",
properties: { path: { type: "string" }, head: { type: "number" } },
required: ["path"],
},
},
];
const definitions = mcpToolsToDefinitions(listed, { namespace: "fs" });
const registry = createRegistry({});
registerMcpTools(registry, definitions);
console.log(definitions.map((d) => `${d.name} "${d.label}"`).join("\n"));
console.log(generateToolsDts(registry));fs.readTextFile "Read Text File"// Generated by CodeFlow — DO NOT EDIT.
// The registry is the only source of truth; this file is a derived artifact.
// registryHash: ef5ca9cc4ea42b98b5570ed2e060b1250df22b1c07e39e6a5efedfca7e38b29b
// Regenerate with `codeflow generate`.
export interface Tools {
fs: {
/** Read a file from disk. Supports '**\/*.md' globs. */
readTextFile(input: { path: string; head?: number }): Promise<unknown>;
};
}What is not mechanical about the mapping
- Names. MCP names are free-form; CodeFlow names are
<namespace>.<method>with identifier segments, because they become property paths onTools.read_text_fileslugs toreadTextFile, collisions get a numeric suffix, and the original is kept indefinition.mcp.toolNameso a runner can call it back. The slugging helpers (slugifyNamespace,slugifyMethod,uniqueMethod,humanize,words,isValidIdentifier) are exported for exactly that round trip. - Labels. MCP
title, elseannotations.title, else a humanized name. A label is what a non-developer reads on a node, so it never falls back to a slug. - Schemas. Passed through verbatim. Converting them here would invent a second representation of something core already understands;
generateToolsDtsturns them into TypeScript at codegen time.
Two bugs that only real servers exposed
Both were found by running 65 live schemas from 8 servers through the pipeline, and both are now permanent offline tests:
- A description containing
*/— Anthropic's own filesystem server documents'**/*.ext'globs — closed the JSDoc comment early and produced 379 TypeScript errors. Build-breaker. (That is the*\/in the output above.) $refin an input schema emitted a type nobody declared. Every zod-based server hits it.$refs are now resolved inline.
Tests
pnpm --filter @codeflow-team/mcp test # 155 tests, all offline