chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
+136
@@ -0,0 +1,136 @@
|
||||
# @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
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
### Tools for an LLM
|
||||
|
||||
`toLlmContent(result)` converts a `CallToolResult` to text and image content for a model, in the shape of `@earendil-works/pi-ai`'s `TextContent` and `ImageContent`. Text and images pass through, embedded text and image resources are unwrapped, and audio, resource links, and binary resources become short text placeholders. A result without content blocks but with `structuredContent` becomes its JSON.
|
||||
|
||||
Wrapping an MCP tool as a `pi-agent-core` `AgentTool`:
|
||||
|
||||
```typescript
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
import { toLlmContent } from "@earendil-works/pi-mcp";
|
||||
import { Type } from "typebox";
|
||||
|
||||
const tools: AgentTool[] = (await client.listTools()).map((tool) => ({
|
||||
// Providers allow at most 64 characters of [A-Za-z0-9_-].
|
||||
name: `mcp_${tool.name}`.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64),
|
||||
label: tool.title ?? tool.name,
|
||||
description: tool.description ?? tool.name,
|
||||
// Providers require an object schema, and some reject one without `properties`.
|
||||
parameters: Type.Unsafe({ ...tool.inputSchema, type: "object", properties: tool.inputSchema.properties ?? {} }),
|
||||
execute: async (_toolCallId, params, signal) => {
|
||||
const result = await client.callTool(tool.name, params as Record<string, unknown>, { signal });
|
||||
// MCP reports tool failures in the result instead of as a protocol error.
|
||||
return { content: toLlmContent(result), details: undefined, isError: result.isError === true };
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
The [mcp-codemode example](https://github.com/earendil-works/pi/tree/main/packages/agent/examples/mcp-codemode) also forwards progress, passes `structuredContent` through, and lets `@earendil-works/pi-codemode` scripts call the tools.
|
||||
|
||||
### OAuth
|
||||
|
||||
`@earendil-works/pi-mcp/oauth` provides the MCP OAuth client subset without depending on the official SDK:
|
||||
|
||||
```typescript
|
||||
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 version `2025-11-25`, accepting servers that negotiate `2025-06-18`, `2025-03-26`, or `2024-11-05`
|
||||
- initialization and `notifications/initialized`
|
||||
- ping
|
||||
- paginated `tools/list`
|
||||
- `tools/call`, including structured content
|
||||
- progress notifications and timeout renewal
|
||||
- request cancellation
|
||||
- Streamable HTTP sessions, the server-to-client GET stream with reconnection, and resumption of dropped response streams with `Last-Event-ID`
|
||||
- stdio shutdown per the spec (close stdin, then SIGTERM, then SIGKILL), applied to the server's whole process group
|
||||
- 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, token refresh (one refresh shared by concurrent 401s), and step-up authorization for `insufficient_scope`
|
||||
|
||||
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.
|
||||
Reference in new issue
Block a user