@eovidiu/pi-extensions 0.1.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.
- package/LICENSE +21 -0
- package/README.md +379 -0
- package/SECURITY.md +13 -0
- package/examples/mcp.json.example +23 -0
- package/examples/project-mcp.override.example.json +17 -0
- package/extensions/mcp-bridge/README.md +51 -0
- package/extensions/mcp-bridge/config-discovery.ts +143 -0
- package/extensions/mcp-bridge/config-sync.ts +242 -0
- package/extensions/mcp-bridge/index.ts +354 -0
- package/extensions/mcp-bridge/logger.ts +35 -0
- package/extensions/mcp-bridge/mcp-client.ts +178 -0
- package/extensions/mcp-bridge/schema-conversion.ts +84 -0
- package/extensions/mcp-bridge/tool-registration.ts +114 -0
- package/extensions/mcp-bridge/types.ts +52 -0
- package/fixtures/claude-code/settings.json +15 -0
- package/fixtures/claude-desktop/claude_desktop_config.json +11 -0
- package/fixtures/codex/config.toml +6 -0
- package/package.json +51 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { getDefaultEnvironment, StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
import type { McpServerConfig, PiMcpConfig } from "./types.js";
|
|
5
|
+
import { logDebug } from "./logger.js";
|
|
6
|
+
|
|
7
|
+
export interface McpToolBinding {
|
|
8
|
+
piToolName: string;
|
|
9
|
+
serverName: string;
|
|
10
|
+
mcpToolName: string;
|
|
11
|
+
description: string;
|
|
12
|
+
inputSchema: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ConnectedServer {
|
|
16
|
+
name: string;
|
|
17
|
+
config: McpServerConfig;
|
|
18
|
+
client: Client;
|
|
19
|
+
transport: StdioClientTransport;
|
|
20
|
+
tools: McpToolBinding[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class McpBridgeRuntime {
|
|
24
|
+
private servers = new Map<string, ConnectedServer>();
|
|
25
|
+
private bindings = new Map<string, McpToolBinding>();
|
|
26
|
+
|
|
27
|
+
getToolBindings(): McpToolBinding[] {
|
|
28
|
+
return [...this.bindings.values()].sort((a, b) => a.piToolName.localeCompare(b.piToolName));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getConnectedServerNames(): string[] {
|
|
32
|
+
return [...this.servers.keys()].sort();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getConnectionSummary(): string {
|
|
36
|
+
const connected = this.getConnectedServerNames();
|
|
37
|
+
const tools = this.getToolBindings();
|
|
38
|
+
return [
|
|
39
|
+
`Connected servers: ${connected.length ? connected.join(", ") : "none"}`,
|
|
40
|
+
`Registered MCP tool bindings: ${tools.length}`,
|
|
41
|
+
...tools.map((tool) => `- ${tool.piToolName} -> ${tool.serverName}/${tool.mcpToolName}`),
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async startEnabled(config: PiMcpConfig): Promise<McpToolBinding[]> {
|
|
46
|
+
const enabled = Object.entries(config.servers).filter(([serverName, server]) => server.enabled && isServerAllowed(serverName, config));
|
|
47
|
+
for (const [serverName, server] of enabled) {
|
|
48
|
+
if (this.servers.has(serverName)) continue;
|
|
49
|
+
try {
|
|
50
|
+
await this.startServer(serverName, server);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
await logDebug("Failed to start MCP server", { serverName, error: errorMessage(error) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return this.getToolBindings();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async restartServer(serverName: string, config: PiMcpConfig): Promise<McpToolBinding[]> {
|
|
59
|
+
await this.stopServer(serverName);
|
|
60
|
+
const server = config.servers[serverName];
|
|
61
|
+
if (!server) throw new Error(`Unknown MCP server: ${serverName}`);
|
|
62
|
+
if (!server.enabled) throw new Error(`MCP server is disabled: ${serverName}`);
|
|
63
|
+
if (!isServerAllowed(serverName, config)) throw new Error(`MCP server is blocked by allow/deny filters: ${serverName}`);
|
|
64
|
+
await this.startServer(serverName, server);
|
|
65
|
+
return this.getToolBindings();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async restartEnabled(config: PiMcpConfig): Promise<McpToolBinding[]> {
|
|
69
|
+
await this.stopAll();
|
|
70
|
+
return this.startEnabled(config);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async stopNotEnabled(config: PiMcpConfig): Promise<string[]> {
|
|
74
|
+
const removed: string[] = [];
|
|
75
|
+
for (const serverName of this.getConnectedServerNames()) {
|
|
76
|
+
if (!config.servers[serverName]?.enabled) removed.push(...await this.stopServer(serverName));
|
|
77
|
+
}
|
|
78
|
+
return removed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async stopServer(serverName: string): Promise<string[]> {
|
|
82
|
+
const existing = this.servers.get(serverName);
|
|
83
|
+
if (!existing) return [];
|
|
84
|
+
this.servers.delete(serverName);
|
|
85
|
+
const removedTools = existing.tools.map((tool) => tool.piToolName);
|
|
86
|
+
for (const toolName of removedTools) this.bindings.delete(toolName);
|
|
87
|
+
try {
|
|
88
|
+
await existing.transport.close();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
await logDebug("Error while closing MCP transport", { serverName, error: errorMessage(error) });
|
|
91
|
+
}
|
|
92
|
+
await logDebug("Stopped MCP server", { serverName, removedTools });
|
|
93
|
+
return removedTools;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async stopAll(): Promise<string[]> {
|
|
97
|
+
const names = this.getConnectedServerNames();
|
|
98
|
+
const removed: string[] = [];
|
|
99
|
+
for (const name of names) removed.push(...await this.stopServer(name));
|
|
100
|
+
return removed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async callTool(piToolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
|
|
104
|
+
const binding = this.bindings.get(piToolName);
|
|
105
|
+
if (!binding) throw new Error(`MCP tool is not active: ${piToolName}`);
|
|
106
|
+
const server = this.servers.get(binding.serverName);
|
|
107
|
+
if (!server) throw new Error(`MCP server is not connected: ${binding.serverName}`);
|
|
108
|
+
return server.client.callTool(
|
|
109
|
+
{ name: binding.mcpToolName, arguments: args },
|
|
110
|
+
undefined,
|
|
111
|
+
signal ? { signal } : undefined,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private async startServer(serverName: string, config: McpServerConfig): Promise<void> {
|
|
116
|
+
const transport = new StdioClientTransport({
|
|
117
|
+
command: config.command,
|
|
118
|
+
args: config.args ?? [],
|
|
119
|
+
env: { ...getDefaultEnvironment(), ...(config.env ?? {}) },
|
|
120
|
+
stderr: "pipe",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
transport.stderr?.on("data", (chunk: Buffer | string) => {
|
|
124
|
+
void logDebug("MCP server stderr", { serverName, stderr: String(chunk).slice(0, 4000) });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const client = new Client({ name: "pi-mcp-bridge", version: "0.1.0" }, { capabilities: {} });
|
|
128
|
+
await client.connect(transport);
|
|
129
|
+
const listed = await client.listTools();
|
|
130
|
+
const tools = listed.tools.map((tool) => ({
|
|
131
|
+
piToolName: this.makePiToolName(serverName, tool.name),
|
|
132
|
+
serverName,
|
|
133
|
+
mcpToolName: tool.name,
|
|
134
|
+
description: tool.description ?? `MCP tool ${tool.name} from ${serverName}`,
|
|
135
|
+
inputSchema: tool.inputSchema,
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
const connected: ConnectedServer = { name: serverName, config, client, transport, tools };
|
|
139
|
+
this.servers.set(serverName, connected);
|
|
140
|
+
for (const tool of tools) this.bindings.set(tool.piToolName, tool);
|
|
141
|
+
await logDebug("Started MCP server", { serverName, toolCount: tools.length, tools: tools.map((tool) => tool.piToolName) });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private makePiToolName(serverName: string, mcpToolName: string): string {
|
|
145
|
+
const base = normalizeToolName(`mcp_${serverName}_${mcpToolName}`);
|
|
146
|
+
if (!this.bindings.has(base)) return base;
|
|
147
|
+
return `${base}_${shortHash(`${serverName}/${mcpToolName}`)}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeToolName(value: string): string {
|
|
152
|
+
return value.replace(/[^A-Za-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "mcp_tool";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function shortHash(value: string): string {
|
|
156
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 6);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isServerAllowed(serverName: string, config: PiMcpConfig): boolean {
|
|
160
|
+
if (config.allowServers?.length && !config.allowServers.some((pattern) => matchesPattern(serverName, pattern))) return false;
|
|
161
|
+
if (config.denyServers?.some((pattern) => matchesPattern(serverName, pattern))) return false;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function matchesPattern(value: string, pattern: string): boolean {
|
|
166
|
+
if (pattern === "*" || pattern === value) return true;
|
|
167
|
+
if (!pattern.includes("*")) return false;
|
|
168
|
+
const escaped = pattern.split("*").map(escapeRegExp).join(".*");
|
|
169
|
+
return new RegExp(`^${escaped}$`).test(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function escapeRegExp(value: string): string {
|
|
173
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function errorMessage(error: unknown): string {
|
|
177
|
+
return error instanceof Error ? error.message : String(error);
|
|
178
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type, type TSchema } from "typebox";
|
|
3
|
+
|
|
4
|
+
interface JsonSchema {
|
|
5
|
+
type?: string | string[];
|
|
6
|
+
description?: string;
|
|
7
|
+
properties?: Record<string, JsonSchema>;
|
|
8
|
+
required?: string[];
|
|
9
|
+
items?: JsonSchema;
|
|
10
|
+
enum?: unknown[];
|
|
11
|
+
additionalProperties?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function convertMcpInputSchema(schema: unknown): TSchema {
|
|
15
|
+
if (!isObject(schema)) throw new Error("inputSchema must be an object");
|
|
16
|
+
const typed = schema as JsonSchema;
|
|
17
|
+
if (schemaType(typed) !== "object") throw new Error("Only object top-level input schemas are supported");
|
|
18
|
+
return convertObjectSchema(typed, "root");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function convertSchema(schema: JsonSchema, path: string): TSchema {
|
|
22
|
+
if (Array.isArray(schema.type)) throw new Error(`Unsupported union type at ${path}`);
|
|
23
|
+
|
|
24
|
+
if (schema.enum) {
|
|
25
|
+
if (schema.enum.length === 0) throw new Error(`Unsupported empty enum at ${path}`);
|
|
26
|
+
if (schema.enum.every((value): value is string => typeof value === "string")) {
|
|
27
|
+
return withDescription(StringEnum(schema.enum as [string, ...string[]]), schema.description);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unsupported non-string enum at ${path}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
switch (schemaType(schema)) {
|
|
33
|
+
case "string":
|
|
34
|
+
return withDescription(Type.String(), schema.description);
|
|
35
|
+
case "number":
|
|
36
|
+
return withDescription(Type.Number(), schema.description);
|
|
37
|
+
case "integer":
|
|
38
|
+
return withDescription(Type.Integer(), schema.description);
|
|
39
|
+
case "boolean":
|
|
40
|
+
return withDescription(Type.Boolean(), schema.description);
|
|
41
|
+
case "array": {
|
|
42
|
+
const items = schema.items ? convertSchema(schema.items, `${path}[]`) : Type.Any();
|
|
43
|
+
return withDescription(Type.Array(items), schema.description);
|
|
44
|
+
}
|
|
45
|
+
case "object":
|
|
46
|
+
return convertObjectSchema(schema, path);
|
|
47
|
+
default:
|
|
48
|
+
throw new Error(`Unsupported or missing type at ${path}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function convertObjectSchema(schema: JsonSchema, path: string): TSchema {
|
|
53
|
+
if (hasComplexCombinators(schema)) throw new Error(`Unsupported schema combinator at ${path}`);
|
|
54
|
+
const required = new Set(schema.required ?? []);
|
|
55
|
+
const props: Record<string, TSchema> = {};
|
|
56
|
+
for (const [key, value] of Object.entries(schema.properties ?? {})) {
|
|
57
|
+
if (hasComplexCombinators(value)) throw new Error(`Unsupported schema combinator at ${path}.${key}`);
|
|
58
|
+
const converted = convertSchema(value, `${path}.${key}`);
|
|
59
|
+
props[key] = required.has(key) ? converted : Type.Optional(converted);
|
|
60
|
+
}
|
|
61
|
+
return withDescription(Type.Object(props), schema.description);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function schemaType(schema: JsonSchema): string | undefined {
|
|
65
|
+
if (Array.isArray(schema.type)) return undefined;
|
|
66
|
+
if (schema.type) return schema.type;
|
|
67
|
+
if (schema.properties) return "object";
|
|
68
|
+
if (schema.items) return "array";
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hasComplexCombinators(schema: unknown): boolean {
|
|
73
|
+
if (!isObject(schema)) return false;
|
|
74
|
+
return "oneOf" in schema || "anyOf" in schema || "allOf" in schema || "$ref" in schema;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function withDescription<T extends TSchema>(schema: T, description: string | undefined): T {
|
|
78
|
+
if (description) (schema as unknown as { description?: string }).description = description;
|
|
79
|
+
return schema;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
83
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
84
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { McpBridgeRuntime, McpToolBinding } from "./mcp-client.js";
|
|
3
|
+
import { convertMcpInputSchema } from "./schema-conversion.js";
|
|
4
|
+
import { logDebug } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
export class McpToolRegistrar {
|
|
7
|
+
private registered = new Set<string>();
|
|
8
|
+
private active = new Set<string>();
|
|
9
|
+
|
|
10
|
+
constructor(private readonly pi: ExtensionAPI, private readonly runtime: McpBridgeRuntime, private maxOutputChars = 20_000) {}
|
|
11
|
+
|
|
12
|
+
setMaxOutputChars(maxOutputChars: number | undefined): void {
|
|
13
|
+
this.maxOutputChars = maxOutputChars && maxOutputChars > 0 ? maxOutputChars : 20_000;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async registerBindings(bindings: McpToolBinding[]): Promise<string[]> {
|
|
17
|
+
const activated: string[] = [];
|
|
18
|
+
for (const binding of bindings) {
|
|
19
|
+
if (!this.registered.has(binding.piToolName)) {
|
|
20
|
+
try {
|
|
21
|
+
this.registerBinding(binding);
|
|
22
|
+
this.registered.add(binding.piToolName);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
await logDebug("Skipped MCP tool with unsupported schema", {
|
|
25
|
+
piToolName: binding.piToolName,
|
|
26
|
+
serverName: binding.serverName,
|
|
27
|
+
mcpToolName: binding.mcpToolName,
|
|
28
|
+
error: errorMessage(error),
|
|
29
|
+
});
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
this.active.add(binding.piToolName);
|
|
34
|
+
activated.push(binding.piToolName);
|
|
35
|
+
}
|
|
36
|
+
this.applyActiveTools();
|
|
37
|
+
return activated;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
deactivateTools(toolNames: string[]): void {
|
|
41
|
+
for (const toolName of toolNames) this.active.delete(toolName);
|
|
42
|
+
this.applyActiveTools();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
deactivateAll(): void {
|
|
46
|
+
this.active.clear();
|
|
47
|
+
this.applyActiveTools();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private registerBinding(binding: McpToolBinding): void {
|
|
51
|
+
const parameters = convertMcpInputSchema(binding.inputSchema);
|
|
52
|
+
this.pi.registerTool({
|
|
53
|
+
name: binding.piToolName,
|
|
54
|
+
label: `MCP ${binding.mcpToolName}`,
|
|
55
|
+
description: `${binding.description}\n\nMCP server: ${binding.serverName}; MCP tool: ${binding.mcpToolName}`,
|
|
56
|
+
promptSnippet: `Call MCP tool ${binding.mcpToolName} from server ${binding.serverName}`,
|
|
57
|
+
promptGuidelines: [`Use ${binding.piToolName} only when the user asks for capability provided by MCP server ${binding.serverName}.`],
|
|
58
|
+
parameters,
|
|
59
|
+
execute: async (_toolCallId, params, signal) => {
|
|
60
|
+
const result = await this.runtime.callTool(binding.piToolName, params as Record<string, unknown>, signal);
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: truncateText(formatMcpResult(result), this.maxOutputChars) }],
|
|
63
|
+
details: { serverName: binding.serverName, mcpToolName: binding.mcpToolName, result },
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private applyActiveTools(): void {
|
|
70
|
+
const active = new Set(this.pi.getActiveTools());
|
|
71
|
+
for (const registered of this.registered) active.delete(registered);
|
|
72
|
+
for (const toolName of this.active) active.add(toolName);
|
|
73
|
+
this.pi.setActiveTools([...active]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function formatMcpResult(result: unknown): string {
|
|
78
|
+
if (isObject(result) && Array.isArray(result.content)) {
|
|
79
|
+
const chunks = result.content.map(formatMcpContent).filter(Boolean);
|
|
80
|
+
if (chunks.length) return chunks.join("\n\n");
|
|
81
|
+
}
|
|
82
|
+
if (isObject(result) && "toolResult" in result) return stringify(result.toolResult);
|
|
83
|
+
return stringify(result);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatMcpContent(content: unknown): string {
|
|
87
|
+
if (!isObject(content)) return stringify(content);
|
|
88
|
+
if (content.type === "text" && typeof content.text === "string") return content.text;
|
|
89
|
+
if (content.type === "image") return `[MCP image content: ${typeof content.mimeType === "string" ? content.mimeType : "unknown mime type"}]`;
|
|
90
|
+
if (content.type === "audio") return `[MCP audio content: ${typeof content.mimeType === "string" ? content.mimeType : "unknown mime type"}]`;
|
|
91
|
+
if (content.type === "resource" && isObject(content.resource)) {
|
|
92
|
+
if (typeof content.resource.text === "string") return content.resource.text;
|
|
93
|
+
if (typeof content.resource.uri === "string") return `[MCP resource: ${content.resource.uri}]`;
|
|
94
|
+
}
|
|
95
|
+
return stringify(content);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function truncateText(text: string, maxChars: number): string {
|
|
99
|
+
if (text.length <= maxChars) return text;
|
|
100
|
+
const omitted = text.length - maxChars;
|
|
101
|
+
return `${text.slice(0, maxChars)}\n\n[Truncated ${omitted} characters from MCP tool output]`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function stringify(value: unknown): string {
|
|
105
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
109
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function errorMessage(error: unknown): string {
|
|
113
|
+
return error instanceof Error ? error.message : String(error);
|
|
114
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const MANAGED_BY = "pi-mcp-bridge";
|
|
2
|
+
|
|
3
|
+
export type McpSource = "claude-desktop" | "claude-code" | "codex" | "manual" | string;
|
|
4
|
+
|
|
5
|
+
export interface McpServerConfig {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
managedBy?: string;
|
|
8
|
+
source?: McpSource;
|
|
9
|
+
sourceName?: string;
|
|
10
|
+
command: string;
|
|
11
|
+
args?: string[];
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PiMcpConfig {
|
|
16
|
+
version: 1;
|
|
17
|
+
autoStart: boolean;
|
|
18
|
+
servers: Record<string, McpServerConfig>;
|
|
19
|
+
allowServers?: string[];
|
|
20
|
+
denyServers?: string[];
|
|
21
|
+
maxOutputChars?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DiscoveredMcpServer extends McpServerConfig {
|
|
25
|
+
managedBy: typeof MANAGED_BY;
|
|
26
|
+
source: McpSource;
|
|
27
|
+
sourceName: string;
|
|
28
|
+
configPath: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DiscoveryEvent {
|
|
32
|
+
path: string;
|
|
33
|
+
source: McpSource;
|
|
34
|
+
status: "missing" | "found" | "parsed" | "contains-mcp" | "unsupported" | "parse-error";
|
|
35
|
+
message?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DiscoveryResult {
|
|
39
|
+
servers: Record<string, DiscoveredMcpServer>;
|
|
40
|
+
events: DiscoveryEvent[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SyncResult {
|
|
44
|
+
configPath: string;
|
|
45
|
+
discoveredCount: number;
|
|
46
|
+
added: string[];
|
|
47
|
+
updated: string[];
|
|
48
|
+
removed: string[];
|
|
49
|
+
preservedManual: string[];
|
|
50
|
+
enabled: string[];
|
|
51
|
+
disabled: string[];
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eovidiu/pi-extensions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Personal Pi extensions, starting with an opt-in MCP sync bridge.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"homepage": "https://github.com/eovidiu/pi-extensions#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/eovidiu/pi-extensions/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/eovidiu/pi-extensions.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pi-package",
|
|
16
|
+
"pi-extension",
|
|
17
|
+
"mcp"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"files": [
|
|
21
|
+
"extensions",
|
|
22
|
+
"examples",
|
|
23
|
+
"fixtures",
|
|
24
|
+
"README.md",
|
|
25
|
+
"SECURITY.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run"
|
|
31
|
+
},
|
|
32
|
+
"pi": {
|
|
33
|
+
"extensions": [
|
|
34
|
+
"./extensions/mcp-bridge"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@iarna/toml": "2.2.5",
|
|
39
|
+
"@modelcontextprotocol/sdk": "1.29.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-ai": "*",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
44
|
+
"typebox": "*"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/node": "^25.8.0",
|
|
48
|
+
"typescript": "^6.0.3",
|
|
49
|
+
"vitest": "^4.1.6"
|
|
50
|
+
}
|
|
51
|
+
}
|