@crow-agent/mcp
v0.2.0
Published
Model Context Protocol client: transports, OAuth, and tool bridging.
Readme
@crow-agent/mcp
A self-contained Model Context Protocol client: no dependency on the official MCP SDK and no dependency on any other Crow package.
It covers the client side of MCP only — there is no server implementation
here. The transport layer (how bytes move) is separated from the client
core (what the bytes mean), so stdio, Streamable HTTP, and in-memory
transports all plug into the same McpClient.
Talking to a server
import { McpClient, StdioTransport } from "@crow-agent/mcp";
const client = new McpClient({ name: "my-client", version: "1.0.0" });
await client.connect(
new StdioTransport({ command: "my-mcp-server", args: ["--stdio"] }),
);
for (const tool of await client.listTools()) {
console.log(tool.name, "-", tool.description);
}
const result = await client.callTool("read_file", { path: "/etc/hosts" });
await client.close();Point at a remote server with StreamableHttpTransport instead:
import { StreamableHttpTransport } from "@crow-agent/mcp";
const transport = new StreamableHttpTransport({
url: "https://mcp.example.com/mcp",
headers: { Authorization: `Bearer ${token}` },
fetch: customFetch, // optional: proxying, custom agents, test doubles
});What the client handles for you
Once connected, McpClient owns the protocol bookkeeping so callers
deal with plain requests and results:
- the
initializehandshake andnotifications/initialized, negotiating the newest mutually supported protocol version (2025-11-25preferred, back to2024-11-05accepted); - correlating requests with responses, timeouts, and cancellation —
including cancelling a long tool call via
notifications/cancelled; - renewing timeouts while progress notifications arrive;
- paginating
tools/listand surfacing structured tool output; - answering incoming
pingandroots/listrequests from the server; - delivering logging and tool-list-changed notifications through a generic notification API;
- Streamable HTTP session management: the server-to-client GET stream,
reconnection, and resuming dropped response streams with
Last-Event-ID.
For stdio servers it also owns shutdown: stdin is closed first, then SIGTERM, then SIGKILL, across the server's whole process group — so a server that ignores termination still gets reaped.
Out of scope by design: batch JSON-RPC, the legacy HTTP+SSE transport, sampling, tasks, and anything server-side.
Handing tools to a model
toLlmContent() turns a CallToolResult into the content blocks a model
accepts (TextContent / ImageContent in @crow-agent/ai's shape). Text and
images pass through, embedded resources are unwrapped, and audio,
resource links, and binary blobs become short text placeholders. A
result with no content blocks but a structuredContent payload becomes
its JSON.
import { toLlmContent } from "@crow-agent/mcp";
const result = await client.callTool("search", { query: "MCP" });
const blocks = toLlmContent(result); // TextContent[] | ImageContent[]
// MCP reports tool failures inside the result, not as protocol errors:
const failed = result.isError === true;Signing in with OAuth
@crow-agent/mcp/oauth implements the MCP OAuth client subset — protected
resource and authorization server discovery, PKCE authorization code
flow, dynamic client registration, token refresh (one refresh shared
across concurrent 401s), and step-up authorization on
insufficient_scope — without the official SDK:
import { McpClient, StreamableHttpTransport } from "@crow-agent/mcp";
import {
adaptOAuthProvider,
authorizeMcp,
McpOAuthAuthorizationRequiredError,
McpOAuthProvider,
OAuthCallbackServer,
} from "@crow-agent/mcp/oauth";
const serverUrl = "https://mcp.example.com/mcp";
const callback = await OAuthCallbackServer.listen();
const oauth = new McpOAuthProvider({
serverUrl,
redirectUrl: callback.redirectUrl,
clientMetadata: { client_name: "My MCP client" },
onRedirect: (url) => openInBrowser(url), // your UI, your browser call
});
async function connectOnce() {
const client = new McpClient({ name: "my-client", version: "1.0.0" });
await client.connect(
new StreamableHttpTransport({ url: serverUrl, authProvider: adaptOAuthProvider(oauth) }),
);
return client;
}
try {
return await connectOnce();
} catch (error) {
if (!(error instanceof McpOAuthAuthorizationRequiredError)) throw error;
const { code } = await callback.waitForCallback(await oauth.state());
await authorizeMcp(oauth, { serverUrl, authorizationCode: code });
return connectOnce();
}The package never opens a browser itself and never decides where
credentials live: pass an McpOAuthStateStore (or the in-memory
MemoryOAuthStateStore) to McpOAuthProvider for durable tokens.
The OAuth implementation is adapted from the MIT-licensed Model Context
Protocol TypeScript SDK; its license text ships under LICENSES/.
Writing tests
@crow-agent/mcp/testing exports createInMemoryTransportPair(), a matched
pair of in-memory transports — drive one end as the server in your test
and connect the client to the other, no subprocesses involved.
