pi-mono/packages/mcp
Armin Ronacher 27d2914457 feat(coding-agent): connect MCP servers and expose them through codemode
Adds a built-in mcp extension that reads mcp.json (agent dir, and .pi/mcp.json for trusted projects) in the common mcpServers format, connects stdio and streamable HTTP servers at session start, and registers their tools as mcp__<server>__<tool>. Calls go through the regular tool pipeline, so hooks and permission extensions apply. /mcp shows server status.

By default a server's tools are codemode-only: the new nestedOnly tool flag keeps them executable for other tools but out of the model's tool declarations, and pi activates the codemode tool for them. "exposure": "direct" declares them to the model as well. structuredContent from tools with an outputSchema reaches codemode scripts as data.
2026-09-25 15:02:51 +02:00
..
LICENSES feat(mcp): add OAuth client support 2026-09-25 15:02:51 +02:00
src refactor(mcp): deduplicate transports, client, and OAuth helpers 2026-09-25 15:02:51 +02:00
test refactor(mcp): deduplicate transports, client, and OAuth helpers 2026-09-25 15:02:51 +02:00
CHANGELOG.md feat(mcp): add standalone MCP client 2026-09-25 15:02:51 +02:00
package.json feat(coding-agent): connect MCP servers and expose them through codemode 2026-09-25 15:02:51 +02:00
README.md feat(mcp): add OAuth client support 2026-09-25 15:02:51 +02:00
tsconfig.build.json feat(mcp): add standalone MCP client 2026-09-25 15:02:51 +02:00
vitest.config.ts feat(mcp): add standalone MCP client 2026-09-25 15:02:51 +02:00

@earendil-works/pi-mcp

A small, standalone Model Context Protocol client. It does not depend on the official MCP SDK or other pi packages.

The package provides a transport-neutral client core, stdio and Streamable HTTP transports, and an in-memory testing transport.

Usage

import { McpClient, StdioTransport } from "@earendil-works/pi-mcp";

const transport = new StdioTransport({
	command: "npx",
	args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"],
});
const client = new McpClient({
	name: "my-client",
	version: "1.0.0",
	roots: [{ uri: "file:///workspace", name: "workspace" }],
});

await client.connect(transport);
const tools = await client.listTools();
const result = await client.callTool("search", { query: "MCP" });
await client.close();

For a remote server, use new StreamableHttpTransport({ url, headers }). Fetch can be injected for proxying or custom networking.

OAuth

@earendil-works/pi-mcp/oauth provides the MCP OAuth client subset without depending on the official SDK:

import { McpClient, StreamableHttpTransport } from "@earendil-works/pi-mcp";
import {
	adaptOAuthProvider,
	authorizeMcp,
	McpOAuthAuthorizationRequiredError,
	McpOAuthProvider,
	OAuthCallbackServer,
} from "@earendil-works/pi-mcp/oauth";

const serverUrl = "https://mcp.example.com/mcp";
const callback = await OAuthCallbackServer.listen();
let authorizationUrl: URL | undefined;
const oauth = new McpOAuthProvider({
	serverUrl,
	redirectUrl: callback.redirectUrl,
	clientMetadata: { client_name: "My MCP client" },
	onRedirect: (url) => {
		authorizationUrl = url;
	},
});

const connect = () => {
	const client = new McpClient({ name: "my-client", version: "1.0.0" });
	return {
		client,
		connected: client.connect(
			new StreamableHttpTransport({ url: serverUrl, authProvider: adaptOAuthProvider(oauth) }),
		),
	};
};

const first = connect();
try {
	await first.connected;
} catch (error) {
	if (!(error instanceof McpOAuthAuthorizationRequiredError)) throw error;
	const state = await oauth.state();
	const result = callback.waitForCallback(state);
	// Open authorizationUrl in the user's browser here.
	const { code } = await result;
	await authorizeMcp(oauth, { serverUrl, authorizationCode: code });
}

const { client, connected } = connect();
await connected;

Inject McpOAuthStateStore into McpOAuthProvider for durable credentials. The package does not open a browser or choose where credentials are stored.

The OAuth implementation is adapted from the MIT-licensed Model Context Protocol TypeScript SDK v1.29.0. Its license is included under LICENSES/.

An MCP transport owns framing and I/O. It delivers individual JSON-RPC messages to McpClient; the client owns request correlation, initialization, timeouts, cancellation, server requests, and protocol-level helpers.

Supported protocol surface

  • MCP protocol versions 2025-06-18 and 2025-03-26
  • initialization and notifications/initialized
  • ping
  • paginated tools/list
  • tools/call, including structured content
  • progress notifications and timeout renewal
  • request cancellation
  • server ping and roots/list requests
  • logging and tool-list-change notifications through the generic notification API
  • OAuth protected-resource and authorization-server discovery
  • PKCE authorization code flow, dynamic client registration, and token refresh

Batch JSON-RPC messages, legacy HTTP+SSE, servers, sampling, and tasks are outside the initial core.

Testing

@earendil-works/pi-mcp/testing exports createInMemoryTransportPair() for client and adapter tests.