@ian-pascoe/pi-mcp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ /* oxlint-disable anti-slop/no-conditional-empty-object-spread -- Exact optional MCP result marker fields are present only when supplied by the protocol operation. */
1
2
  import { createHash } from "node:crypto";
2
3
  import {
3
4
  fromJsonSchema,
@@ -12,27 +13,22 @@ import type {
12
13
  ToolDefinition,
13
14
  ToolResultEvent,
14
15
  } from "@earendil-works/pi-coding-agent";
15
- import { Type, type TSchema } from "typebox";
16
- import { Value } from "typebox/value";
16
+ import type { TSchema } from "typebox";
17
+ import {
18
+ parseMcpResultDetails,
19
+ renderMcpResourceToolCall,
20
+ renderMcpServerToolCall,
21
+ renderMcpToolResult,
22
+ type McpPresentationRedactor,
23
+ type McpResultDetails,
24
+ type McpResultMarker,
25
+ } from "./mcp-presentation.js";
17
26
 
18
27
  const RESOURCE_TOOL_NAMES = [
19
28
  "list_mcp_resources",
20
29
  "list_mcp_resource_templates",
21
30
  "read_mcp_resource",
22
31
  ] as const;
23
- const MCP_DETAILS_OWNER = "pi-mcp";
24
- const McpResultDetailsMarkerSchema = Type.Object(
25
- {
26
- mcp: Type.Object(
27
- {
28
- isError: Type.Boolean(),
29
- owner: Type.Literal(MCP_DETAILS_OWNER),
30
- },
31
- { additionalProperties: true },
32
- ),
33
- },
34
- { additionalProperties: true },
35
- );
36
32
 
37
33
  const ListResourcesSchema = {
38
34
  type: "object",
@@ -147,21 +143,6 @@ interface CompiledServerTool {
147
143
  readonly serverId: string;
148
144
  }
149
145
 
150
- interface McpResultMarker {
151
- isError: boolean;
152
- operation: string;
153
- outputSchemaError?: string;
154
- outputSchemaValid?: boolean;
155
- owner: typeof MCP_DETAILS_OWNER;
156
- serverId?: string;
157
- toolName?: string;
158
- }
159
-
160
- interface McpResultDetails {
161
- readonly mcp: McpResultMarker;
162
- readonly result: JSONValue | undefined;
163
- }
164
-
165
146
  /** Expected invalid-input failure raised through Pi's required throwing tool boundary. */
166
147
  export class McpServerToolInputError extends Error {
167
148
  readonly _tag = "McpServerToolInputError" as const;
@@ -202,13 +183,6 @@ function collisionName(
202
183
  throw new Error(`Pi MCP Server Tool name hash collision for ${baseName}`);
203
184
  }
204
185
 
205
- // oxlint-disable-next-line anti-slop/no-unknown-parameters -- This parser owns Pi's untyped custom tool-result details boundary.
206
- function parseMcpResultDetails(input: unknown): McpResultDetails | undefined {
207
- if (!Value.Check(McpResultDetailsMarkerSchema, input)) return undefined;
208
- // SAFETY: The marker schema established the fields read by the result hook; this module created all matching details objects.
209
- return input as McpResultDetails;
210
- }
211
-
212
186
  function execution(
213
187
  toolCallId: string,
214
188
  signal: AbortSignal | undefined,
@@ -232,6 +206,7 @@ export class McpToolCatalog {
232
206
  constructor(
233
207
  private readonly pi: McpToolCatalogPi,
234
208
  private readonly runtime: McpToolCatalogRuntime,
209
+ private readonly redact: McpPresentationRedactor = (text) => text,
235
210
  ) {
236
211
  this.registerResourceTools();
237
212
  this.pi.on("tool_result", (event) => {
@@ -335,6 +310,17 @@ export class McpToolCatalog {
335
310
  description:
336
311
  compiled.definition.description ?? `Call MCP Server Tool ${compiled.definition.name}.`,
337
312
  parameters,
313
+ renderCall: (arguments_, theme, context) =>
314
+ renderMcpServerToolCall(
315
+ compiled.serverId,
316
+ compiled.definition.name,
317
+ arguments_,
318
+ theme,
319
+ context.expanded,
320
+ this.redact,
321
+ ),
322
+ renderResult: (result, options, theme, context) =>
323
+ renderMcpToolResult(result, options, theme, context.isError, this.redact),
338
324
  execute: async (toolCallId, arguments_, signal, onUpdate, context) => {
339
325
  const parsed = await compiled.inputValidator["~standard"].validate(arguments_);
340
326
  if (parsed.issues !== undefined) {
@@ -385,14 +371,13 @@ export class McpToolCatalog {
385
371
  const mcp: McpResultMarker = {
386
372
  isError: result.isError ?? false,
387
373
  operation,
388
- owner: MCP_DETAILS_OWNER,
374
+ ...(outputSchemaError === undefined ? {} : { outputSchemaError }),
375
+ ...(outputSchemaValid === undefined ? {} : { outputSchemaValid }),
376
+ owner: "pi-mcp",
377
+ ...(compiled === undefined
378
+ ? {}
379
+ : { serverId: compiled.serverId, toolName: compiled.definition.name }),
389
380
  };
390
- if (outputSchemaError !== undefined) mcp.outputSchemaError = outputSchemaError;
391
- if (outputSchemaValid !== undefined) mcp.outputSchemaValid = outputSchemaValid;
392
- if (compiled !== undefined) {
393
- mcp.serverId = compiled.serverId;
394
- mcp.toolName = compiled.definition.name;
395
- }
396
381
  return { content, details: { mcp, result: result.details } };
397
382
  }
398
383
 
@@ -402,6 +387,16 @@ export class McpToolCatalog {
402
387
  label: "List MCP Resources",
403
388
  description: "List Resources advertised by connected MCP Servers.",
404
389
  parameters: ListResourcesSchema,
390
+ renderCall: (parameters, theme, context) =>
391
+ renderMcpResourceToolCall(
392
+ "list_resources",
393
+ parameters,
394
+ theme,
395
+ context.expanded,
396
+ this.redact,
397
+ ),
398
+ renderResult: (result, options, theme, context) =>
399
+ renderMcpToolResult(result, options, theme, context.isError, this.redact),
405
400
  execute: async (toolCallId, parameters, signal, onUpdate, context) =>
406
401
  this.mapOperationResult(
407
402
  await this.runtime.listResources(
@@ -416,6 +411,16 @@ export class McpToolCatalog {
416
411
  label: "List MCP Resource Templates",
417
412
  description: "List Resource Templates advertised by connected MCP Servers.",
418
413
  parameters: ListResourcesSchema,
414
+ renderCall: (parameters, theme, context) =>
415
+ renderMcpResourceToolCall(
416
+ "list_resource_templates",
417
+ parameters,
418
+ theme,
419
+ context.expanded,
420
+ this.redact,
421
+ ),
422
+ renderResult: (result, options, theme, context) =>
423
+ renderMcpToolResult(result, options, theme, context.isError, this.redact),
419
424
  execute: async (toolCallId, parameters, signal, onUpdate, context) =>
420
425
  this.mapOperationResult(
421
426
  await this.runtime.listResourceTemplates(
@@ -430,6 +435,16 @@ export class McpToolCatalog {
430
435
  label: "Read MCP Resource",
431
436
  description: "Read one Resource from a connected MCP Server.",
432
437
  parameters: ReadResourceSchema,
438
+ renderCall: (parameters, theme, context) =>
439
+ renderMcpResourceToolCall(
440
+ "read_resource",
441
+ parameters,
442
+ theme,
443
+ context.expanded,
444
+ this.redact,
445
+ ),
446
+ renderResult: (result, options, theme, context) =>
447
+ renderMcpToolResult(result, options, theme, context.isError, this.redact),
433
448
  execute: async (toolCallId, parameters, signal, onUpdate, context) =>
434
449
  this.mapOperationResult(
435
450
  await this.runtime.readResource(
package/src/pi-mcp-cli.ts CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  // oxlint-disable anti-slop/no-conditional-empty-object-spread -- Exact optional command data requires omitting absent fields at the shared command boundary.
4
4
  // oxlint-disable anti-slop/no-runtime-typeof, anti-slop/no-unknown-parameters -- This entrypoint owns trust-store parsing before values reach typed settings adapters.
5
+ import { realpathSync } from "node:fs";
5
6
  import { readFile } from "node:fs/promises";
6
7
  import { homedir } from "node:os";
7
8
  import { dirname, join, resolve } from "node:path";
8
9
  import { createInterface } from "node:readline/promises";
9
- import { pathToFileURL } from "node:url";
10
+ import { fileURLToPath } from "node:url";
10
11
  import {
11
12
  runMcpCommandTokens,
12
13
  type McpCommandAdapterResult,
@@ -151,7 +152,7 @@ async function listStandaloneServers(state: StandaloneMcpState): Promise<McpComm
151
152
  transport: definition.transport,
152
153
  });
153
154
  messages.push(
154
- `${definition.id} (${definition.provenance}, ${definition.enabled ? "enabled" : "disabled"})`,
155
+ `${definition.id} (provenance=${definition.provenance}, ${definition.enabled ? "enabled" : "disabled"}, transport=${definition.transport}, auth=${definition.transport === "stdio" ? "none" : (definition.auth?.type ?? "anonymous")}, stored-auth=${storedAuth ? "present" : "absent"})`,
155
156
  );
156
157
  }
157
158
  for (const mask of settings.masks.values()) {
@@ -162,7 +163,9 @@ async function listStandaloneServers(state: StandaloneMcpState): Promise<McpComm
162
163
  name: mask.id,
163
164
  provenance: mask.provenance,
164
165
  });
165
- messages.push(`${mask.id} (${mask.provenance}, disabled mask)`);
166
+ messages.push(
167
+ `${mask.id} (provenance=${mask.provenance}, masked, inherited=${mask.inherited ? "yes" : "no"})`,
168
+ );
166
169
  }
167
170
  const message =
168
171
  messages.length === 0 ? "No MCP Server Definitions configured" : messages.join("\n");
@@ -494,7 +497,7 @@ export async function runPiMcpCli(
494
497
  }
495
498
 
496
499
  const entrypoint = process.argv[1];
497
- if (entrypoint !== undefined && import.meta.url === pathToFileURL(entrypoint).href) {
500
+ if (entrypoint !== undefined && fileURLToPath(import.meta.url) === realpathSync(entrypoint)) {
498
501
  void runPiMcpCli(process.argv.slice(2)).then(
499
502
  (exitCode) => {
500
503
  process.exitCode = exitCode;