@aexol/spectral 0.2.5 → 0.2.7
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/dist/cli.js +10 -47
- package/dist/mcp/agent-dir.js +18 -0
- package/dist/mcp/app-bridge.bundle.js +67 -0
- package/dist/mcp/commands.js +263 -0
- package/dist/mcp/config.js +532 -0
- package/dist/mcp/consent-manager.js +59 -0
- package/dist/mcp/direct-tools.js +354 -0
- package/dist/mcp/errors.js +165 -0
- package/dist/mcp/glimpse-ui.js +67 -0
- package/dist/mcp/host-html-template.js +412 -0
- package/dist/mcp/index.js +291 -0
- package/dist/mcp/init.js +280 -0
- package/dist/mcp/lifecycle.js +79 -0
- package/dist/mcp/logger.js +130 -0
- package/dist/mcp/mcp-auth-flow.js +283 -0
- package/dist/mcp/mcp-auth.js +226 -0
- package/dist/mcp/mcp-callback-server.js +225 -0
- package/dist/mcp/mcp-oauth-provider.js +243 -0
- package/dist/mcp/mcp-panel.js +646 -0
- package/dist/mcp/mcp-setup-panel.js +485 -0
- package/dist/mcp/metadata-cache.js +158 -0
- package/dist/mcp/npx-resolver.js +385 -0
- package/dist/mcp/oauth-handler.js +54 -0
- package/dist/mcp/onboarding-state.js +56 -0
- package/dist/mcp/proxy-modes.js +714 -0
- package/dist/mcp/resource-tools.js +14 -0
- package/dist/mcp/sampling-handler.js +206 -0
- package/dist/mcp/server-manager.js +301 -0
- package/dist/mcp/state.js +1 -0
- package/dist/mcp/tool-metadata.js +128 -0
- package/dist/mcp/tool-registrar.js +43 -0
- package/dist/mcp/types.js +93 -0
- package/dist/mcp/ui-resource-handler.js +113 -0
- package/dist/mcp/ui-server.js +522 -0
- package/dist/mcp/ui-session.js +306 -0
- package/dist/mcp/ui-stream-types.js +58 -0
- package/dist/mcp/utils.js +104 -0
- package/dist/mcp/vitest.config.js +13 -0
- package/dist/server/pi-bridge.js +9 -30
- package/package.json +6 -3
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { ensureCompatibilityImports, getMcpDiscoverySummary, getServerProvenance, previewCompatibilityImports, previewSharedServerEntry, previewStarterProjectConfig, writeDirectToolsConfig, writeSharedServerEntry, writeStarterProjectConfig, } from "./config.js";
|
|
2
|
+
import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.js";
|
|
3
|
+
import { loadMetadataCache } from "./metadata-cache.js";
|
|
4
|
+
import { buildToolMetadata } from "./tool-metadata.js";
|
|
5
|
+
import { supportsOAuth, authenticate } from "./mcp-auth-flow.js";
|
|
6
|
+
import { hasStoredTokens } from "./mcp-auth.js";
|
|
7
|
+
import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.js";
|
|
8
|
+
import { openPath } from "./utils.js";
|
|
9
|
+
export async function showStatus(state, ctx) {
|
|
10
|
+
if (!ctx.hasUI)
|
|
11
|
+
return;
|
|
12
|
+
const lines = ["MCP Server Status:", ""];
|
|
13
|
+
for (const name of Object.keys(state.config.mcpServers)) {
|
|
14
|
+
const connection = state.manager.getConnection(name);
|
|
15
|
+
const metadata = state.toolMetadata.get(name);
|
|
16
|
+
const toolCount = metadata?.length ?? 0;
|
|
17
|
+
const failedAgo = getFailureAgeSeconds(state, name);
|
|
18
|
+
let status = "not connected";
|
|
19
|
+
let statusIcon = "○";
|
|
20
|
+
let failed = false;
|
|
21
|
+
if (connection?.status === "connected") {
|
|
22
|
+
status = "connected";
|
|
23
|
+
statusIcon = "✓";
|
|
24
|
+
}
|
|
25
|
+
else if (connection?.status === "needs-auth") {
|
|
26
|
+
status = "needs auth";
|
|
27
|
+
statusIcon = "⚠";
|
|
28
|
+
}
|
|
29
|
+
else if (failedAgo !== null) {
|
|
30
|
+
status = `failed ${failedAgo}s ago`;
|
|
31
|
+
statusIcon = "✗";
|
|
32
|
+
failed = true;
|
|
33
|
+
}
|
|
34
|
+
else if (metadata !== undefined) {
|
|
35
|
+
status = "cached";
|
|
36
|
+
}
|
|
37
|
+
const toolSuffix = failed ? "" : ` (${toolCount} tools${status === "cached" ? ", cached" : ""})`;
|
|
38
|
+
lines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);
|
|
39
|
+
}
|
|
40
|
+
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
41
|
+
lines.push("No MCP servers configured");
|
|
42
|
+
lines.push("Run /mcp setup to adopt imports or scaffold a starter .mcp.json");
|
|
43
|
+
}
|
|
44
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
45
|
+
}
|
|
46
|
+
export async function showTools(state, ctx) {
|
|
47
|
+
if (!ctx.hasUI)
|
|
48
|
+
return;
|
|
49
|
+
const allTools = [...state.toolMetadata.values()].flat().map(m => m.name);
|
|
50
|
+
if (allTools.length === 0) {
|
|
51
|
+
ctx.ui.notify("No MCP tools available", "info");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const lines = [
|
|
55
|
+
"MCP Tools:",
|
|
56
|
+
"",
|
|
57
|
+
...allTools.map(t => ` ${t}`),
|
|
58
|
+
"",
|
|
59
|
+
`Total: ${allTools.length} tools`,
|
|
60
|
+
];
|
|
61
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
62
|
+
}
|
|
63
|
+
export async function reconnectServers(state, ctx, targetServer) {
|
|
64
|
+
if (targetServer && !state.config.mcpServers[targetServer]) {
|
|
65
|
+
if (ctx.hasUI) {
|
|
66
|
+
ctx.ui.notify(`Server "${targetServer}" not found in config`, "error");
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const entries = targetServer
|
|
71
|
+
? [[targetServer, state.config.mcpServers[targetServer]]]
|
|
72
|
+
: Object.entries(state.config.mcpServers);
|
|
73
|
+
for (const [name, definition] of entries) {
|
|
74
|
+
try {
|
|
75
|
+
await state.manager.close(name);
|
|
76
|
+
const connection = await state.manager.connect(name, definition);
|
|
77
|
+
if (connection.status === "needs-auth") {
|
|
78
|
+
if (ctx.hasUI) {
|
|
79
|
+
ctx.ui.notify(`MCP: ${name} requires OAuth. Run /mcp-auth ${name} first.`, "warning");
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
84
|
+
const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
|
|
85
|
+
state.toolMetadata.set(name, metadata);
|
|
86
|
+
updateMetadataCache(state, name);
|
|
87
|
+
state.failureTracker.delete(name);
|
|
88
|
+
if (ctx.hasUI) {
|
|
89
|
+
ctx.ui.notify(`MCP: Reconnected to ${name} (${connection.tools.length} tools, ${connection.resources.length} resources)`, "info");
|
|
90
|
+
if (failedTools.length > 0) {
|
|
91
|
+
ctx.ui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, "warning");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
97
|
+
state.failureTracker.set(name, Date.now());
|
|
98
|
+
if (ctx.hasUI) {
|
|
99
|
+
ctx.ui.notify(`MCP: Failed to reconnect to ${name}: ${message}`, "error");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
updateStatusBar(state);
|
|
104
|
+
}
|
|
105
|
+
export async function authenticateServer(serverName, config, ctx) {
|
|
106
|
+
if (!ctx.hasUI)
|
|
107
|
+
return;
|
|
108
|
+
const definition = config.mcpServers[serverName];
|
|
109
|
+
if (!definition) {
|
|
110
|
+
ctx.ui.notify(`Server "${serverName}" not found in config`, "error");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (!supportsOAuth(definition)) {
|
|
114
|
+
ctx.ui.notify(`Server "${serverName}" does not use OAuth authentication.\n` +
|
|
115
|
+
`Set "auth": "oauth" or omit auth for auto-detection.`, "error");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!definition.url) {
|
|
119
|
+
ctx.ui.notify(`Server "${serverName}" has no URL configured (OAuth requires HTTP transport)`, "error");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
ctx.ui.setStatus("mcp-auth", `Authenticating ${serverName}...`);
|
|
124
|
+
const status = await authenticate(serverName, definition.url, definition);
|
|
125
|
+
if (status === "authenticated") {
|
|
126
|
+
ctx.ui.notify(`OAuth authentication successful for "${serverName}"!\n` +
|
|
127
|
+
`Run /mcp reconnect ${serverName} to connect with the new token.`, "info");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
ctx.ui.notify(`OAuth authentication failed for "${serverName}".`, "error");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
ctx.ui.notify(`Failed to authenticate "${serverName}": ${message}`, "error");
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
ctx.ui.setStatus("mcp-auth", undefined);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function buildSharedConfigNoticeLines(configOverridePath) {
|
|
142
|
+
const discovery = getMcpDiscoverySummary(configOverridePath);
|
|
143
|
+
const onboardingState = loadOnboardingState();
|
|
144
|
+
if (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {
|
|
145
|
+
return { lines: [], fingerprint: null };
|
|
146
|
+
}
|
|
147
|
+
const sharedSources = discovery.sources.filter((source) => source.kind === "shared" && source.serverCount > 0);
|
|
148
|
+
const sourceList = sharedSources.map((source) => source.path).join(", ");
|
|
149
|
+
return {
|
|
150
|
+
lines: [
|
|
151
|
+
`Using standard MCP config from ${sourceList}.`,
|
|
152
|
+
"Pi only writes compatibility imports and adapter-specific overrides into Pi-owned files when needed.",
|
|
153
|
+
],
|
|
154
|
+
fingerprint: discovery.fingerprint,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export async function openMcpSetup(_state, pi, ctx, configOverridePath, mode = "setup") {
|
|
158
|
+
if (!ctx.hasUI)
|
|
159
|
+
return { configChanged: false };
|
|
160
|
+
const discovery = getMcpDiscoverySummary(configOverridePath);
|
|
161
|
+
const onboardingState = loadOnboardingState();
|
|
162
|
+
const { createMcpSetupPanel } = await import("./mcp-setup-panel.js");
|
|
163
|
+
let configChanged = false;
|
|
164
|
+
const callbacks = {
|
|
165
|
+
previewImports: (imports) => previewCompatibilityImports(imports, configOverridePath),
|
|
166
|
+
previewStarterProject: () => previewStarterProjectConfig(),
|
|
167
|
+
previewRepoPrompt: () => {
|
|
168
|
+
const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
|
|
169
|
+
if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName)
|
|
170
|
+
return null;
|
|
171
|
+
return previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
|
|
172
|
+
},
|
|
173
|
+
adoptImports: async (imports) => {
|
|
174
|
+
const result = ensureCompatibilityImports(imports, configOverridePath);
|
|
175
|
+
if (result.added.length > 0)
|
|
176
|
+
configChanged = true;
|
|
177
|
+
return result;
|
|
178
|
+
},
|
|
179
|
+
scaffoldProjectConfig: async () => {
|
|
180
|
+
const path = writeStarterProjectConfig();
|
|
181
|
+
configChanged = true;
|
|
182
|
+
return { path };
|
|
183
|
+
},
|
|
184
|
+
addRepoPrompt: async () => {
|
|
185
|
+
const repoPrompt = getMcpDiscoverySummary(configOverridePath).repoPrompt;
|
|
186
|
+
if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {
|
|
187
|
+
throw new Error("RepoPrompt is not available to add from this setup screen.");
|
|
188
|
+
}
|
|
189
|
+
const path = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
|
|
190
|
+
configChanged = true;
|
|
191
|
+
return { path, serverName: repoPrompt.serverName };
|
|
192
|
+
},
|
|
193
|
+
openPath: async (targetPath) => {
|
|
194
|
+
await openPath(pi, targetPath);
|
|
195
|
+
},
|
|
196
|
+
markSetupCompleted: () => {
|
|
197
|
+
persistSetupCompleted(discovery.fingerprint);
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
ctx.ui.custom((tui, _theme, _keybindings, done) => {
|
|
202
|
+
return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState }, tui, () => {
|
|
203
|
+
done();
|
|
204
|
+
resolve({ configChanged });
|
|
205
|
+
});
|
|
206
|
+
}, { overlay: true, overlayOptions: { anchor: "center", width: 92 } });
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
export async function openMcpPanel(state, pi, ctx, configOverridePath) {
|
|
210
|
+
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
211
|
+
return openMcpSetup(state, pi, ctx, configOverridePath, "empty");
|
|
212
|
+
}
|
|
213
|
+
const config = state.config;
|
|
214
|
+
const cache = loadMetadataCache();
|
|
215
|
+
const provenanceMap = getServerProvenance(pi.getFlag("mcp-config") ?? configOverridePath);
|
|
216
|
+
const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(pi.getFlag("mcp-config") ?? configOverridePath);
|
|
217
|
+
const callbacks = {
|
|
218
|
+
reconnect: async (serverName) => {
|
|
219
|
+
return lazyConnect(state, serverName);
|
|
220
|
+
},
|
|
221
|
+
getConnectionStatus: (serverName) => {
|
|
222
|
+
const definition = config.mcpServers[serverName];
|
|
223
|
+
const connection = state.manager.getConnection(serverName);
|
|
224
|
+
if (connection?.status === "needs-auth") {
|
|
225
|
+
return "needs-auth";
|
|
226
|
+
}
|
|
227
|
+
if (definition?.auth === "oauth"
|
|
228
|
+
&& definition.oauth !== false
|
|
229
|
+
&& definition.oauth?.grantType !== "client_credentials"
|
|
230
|
+
&& !hasStoredTokens(serverName)) {
|
|
231
|
+
return "needs-auth";
|
|
232
|
+
}
|
|
233
|
+
if (connection?.status === "connected")
|
|
234
|
+
return "connected";
|
|
235
|
+
if (getFailureAgeSeconds(state, serverName) !== null)
|
|
236
|
+
return "failed";
|
|
237
|
+
return "idle";
|
|
238
|
+
},
|
|
239
|
+
refreshCacheAfterReconnect: (serverName) => {
|
|
240
|
+
const freshCache = loadMetadataCache();
|
|
241
|
+
return freshCache?.servers?.[serverName] ?? null;
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
const { createMcpPanel } = await import("./mcp-panel.js");
|
|
245
|
+
let configChanged = false;
|
|
246
|
+
await new Promise((resolve) => {
|
|
247
|
+
ctx.ui.custom((tui, _theme, _keybindings, done) => {
|
|
248
|
+
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result) => {
|
|
249
|
+
if (!result.cancelled && result.changes.size > 0) {
|
|
250
|
+
writeDirectToolsConfig(result.changes, provenanceMap, config);
|
|
251
|
+
configChanged = true;
|
|
252
|
+
ctx.ui.notify("Direct tools updated. Pi will reload after this panel closes.", "info");
|
|
253
|
+
}
|
|
254
|
+
done();
|
|
255
|
+
resolve();
|
|
256
|
+
}, { noticeLines });
|
|
257
|
+
}, { overlay: true, overlayOptions: { anchor: "center", width: 82 } });
|
|
258
|
+
});
|
|
259
|
+
if (noticeLines.length > 0 && fingerprint) {
|
|
260
|
+
markSharedConfigHintShown(fingerprint);
|
|
261
|
+
}
|
|
262
|
+
return { configChanged };
|
|
263
|
+
}
|