feat(mcp): shared tool naming, schema, output limit and config helpers

createMcpToolName, mcpNamespace, normalizeMcpInputSchema,
limitMcpContent (with a caller-provided saver) and validation of the
standard mcpServers shape move from coding-agent into pi-mcp, so other
applications name and limit MCP tools exactly like pi. Applications
validate their own fields on the tagged result; coding-agent keeps
exposure, auth.provider and its Client ID Metadata Document rule.
This commit is contained in:
Armin Ronacher 2026-11-03 01:51:23 +01:00
parent bb266bf359
commit 76c040a2ad
21 changed files with 804 additions and 305 deletions

View file

@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { renderToolOutputType } from "@earendil-works/pi-codemode/declarations";
import {
createMcpToolName,
type JsonRpcMessage,
LATEST_PROTOCOL_VERSION,
McpAuthRequiredError,
@ -14,7 +15,6 @@ import {
import { createInMemoryTransportPair, type InMemoryTransport } from "@earendil-works/pi-mcp/testing";
import { afterEach, describe, expect, it } from "vitest";
import { InMemoryAuthStorageBackend } from "../src/core/auth-storage.ts";
import { truncateMiddle } from "../src/core/tools/truncate.ts";
import {
addMcpServerConfig,
getMcpToolExposure,
@ -29,7 +29,7 @@ import {
McpServerConnection,
McpServerLog,
} from "../src/extensions/mcp/runtime.ts";
import { convertMcpResult, createMcpResultSchema, createMcpToolName } from "../src/extensions/mcp/tools.ts";
import { convertMcpResult, createMcpResultSchema } from "../src/extensions/mcp/tools.ts";
// Config values are resolved at connect time, so the literal reference must survive loading.
// biome-ignore lint/suspicious/noTemplateCurlyInString: literal config value reference
@ -448,18 +448,6 @@ describe("MCP tools", () => {
await convertMcpResult("docs", "small", { content: [{ type: "text", text: "ok" }] }, { saveOutput });
expect(saved).toHaveLength(1);
});
it("cuts multi-byte text only at character boundaries", () => {
const text = `${"é".repeat(20_000)}end`;
const result = truncateMiddle(text, 1001);
expect(result.truncated).toBe(true);
expect(result.content).not.toContain("\uFFFD");
expect(result.content.endsWith("end")).toBe(true);
const [head, tail] = result.content.split(/…\d+ chars truncated…/);
expect(Buffer.byteLength(head)).toBeLessThanOrEqual(500);
expect(Buffer.byteLength(tail)).toBeLessThanOrEqual(501);
expect(Array.from(head).length + Array.from(tail).length + result.removedChars).toBe(Array.from(text).length);
});
});
describe("MCP connections", () => {

View file

@ -1,7 +1,7 @@
import { readFileSync, rmSync } from "node:fs";
import { fauxAssistantMessage, fauxToolCall } from "@earendil-works/pi-ai";
import type { SystemMessage, ToolResultMessage } from "@earendil-works/pi-ai/compat";
import { type JsonRpcRequest, LATEST_PROTOCOL_VERSION } from "@earendil-works/pi-mcp";
import { createMcpToolName, type JsonRpcRequest, LATEST_PROTOCOL_VERSION } from "@earendil-works/pi-mcp";
import { createInMemoryTransportPair } from "@earendil-works/pi-mcp/testing";
import { Type } from "typebox";
import { afterEach, describe, expect, it, vi } from "vitest";
@ -10,7 +10,6 @@ import type { SessionManager } from "../../src/core/session-manager.ts";
import { createCodemodeExtension } from "../../src/extensions/codemode/index.ts";
import type { McpExposure, McpServerEntry } from "../../src/extensions/mcp/config.ts";
import { createMcpExtension, MCP_SERVERS_SECTION } from "../../src/extensions/mcp/index.ts";
import { createMcpToolName } from "../../src/extensions/mcp/tools.ts";
import { createToolSearchExtension } from "../../src/extensions/tool-search/index.ts";
import { TOOL_SEARCH_DESCRIPTION } from "../../src/extensions/tool-search/tool.ts";
import { createTestExtensionsResult, createTestResourceLoader } from "../utilities.ts";