waypoi 0.0.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/.github/instructions/ui.instructions.md +42 -0
- package/.github/workflows/ci.yml +35 -0
- package/.github/workflows/publish.yml +71 -0
- package/.github/workflows/release.yml +48 -0
- package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
- package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
- package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
- package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
- package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
- package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
- package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
- package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +131 -0
- package/README.md +552 -0
- package/assets/agent-mode.png +0 -0
- package/assets/categorize.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/endpoint-proxy.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/mcp-generate-image.png +0 -0
- package/assets/mcp-understand-image.png +0 -0
- package/assets/peek-token-flow.png +0 -0
- package/assets/playground.png +0 -0
- package/assets/sankey.png +0 -0
- package/cli/index.ts +2805 -0
- package/cli/legacyRewrite.ts +108 -0
- package/cli/modelRef.ts +24 -0
- package/dist/cli/index.js +2536 -0
- package/dist/cli/legacyRewrite.js +92 -0
- package/dist/cli/modelRef.js +20 -0
- package/dist/src/benchmark/artifacts.js +131 -0
- package/dist/src/benchmark/capabilityClassifier.js +81 -0
- package/dist/src/benchmark/capabilityStore.js +144 -0
- package/dist/src/benchmark/config.js +238 -0
- package/dist/src/benchmark/gates.js +118 -0
- package/dist/src/benchmark/jobs.js +252 -0
- package/dist/src/benchmark/runner.js +1847 -0
- package/dist/src/benchmark/schema.js +353 -0
- package/dist/src/benchmark/suites.js +314 -0
- package/dist/src/benchmark/tinyQaDataset.js +422 -0
- package/dist/src/benchmark/types.js +25 -0
- package/dist/src/config.js +47 -0
- package/dist/src/index.js +178 -0
- package/dist/src/mcp/client.js +215 -0
- package/dist/src/mcp/discovery.js +226 -0
- package/dist/src/mcp/policy.js +65 -0
- package/dist/src/mcp/registry.js +129 -0
- package/dist/src/mcp/service.js +460 -0
- package/dist/src/middleware/auth.js +179 -0
- package/dist/src/middleware/requestCapture.js +192 -0
- package/dist/src/middleware/requestStats.js +118 -0
- package/dist/src/pools/builder.js +132 -0
- package/dist/src/pools/repository.js +69 -0
- package/dist/src/pools/scheduler.js +360 -0
- package/dist/src/pools/types.js +2 -0
- package/dist/src/protocols/adapters/dashscope.js +267 -0
- package/dist/src/protocols/adapters/inferenceV2.js +346 -0
- package/dist/src/protocols/adapters/openai.js +27 -0
- package/dist/src/protocols/registry.js +99 -0
- package/dist/src/protocols/types.js +2 -0
- package/dist/src/providers/health.js +153 -0
- package/dist/src/providers/importer.js +289 -0
- package/dist/src/providers/modelRegistry.js +313 -0
- package/dist/src/providers/repository.js +361 -0
- package/dist/src/providers/types.js +2 -0
- package/dist/src/routes/admin.js +531 -0
- package/dist/src/routes/audio.js +295 -0
- package/dist/src/routes/chat.js +240 -0
- package/dist/src/routes/embeddings.js +157 -0
- package/dist/src/routes/images.js +288 -0
- package/dist/src/routes/mcp.js +256 -0
- package/dist/src/routes/mcpService.js +100 -0
- package/dist/src/routes/models.js +48 -0
- package/dist/src/routes/responses.js +711 -0
- package/dist/src/routes/sessions.js +450 -0
- package/dist/src/routes/stats.js +270 -0
- package/dist/src/routes/ui.js +97 -0
- package/dist/src/routes/videos.js +107 -0
- package/dist/src/routing/router.js +338 -0
- package/dist/src/services/imageGeneration.js +280 -0
- package/dist/src/services/imageUnderstanding.js +352 -0
- package/dist/src/services/videoGeneration.js +79 -0
- package/dist/src/storage/captureRepository.js +1591 -0
- package/dist/src/storage/files.js +157 -0
- package/dist/src/storage/imageCache.js +346 -0
- package/dist/src/storage/repositories.js +388 -0
- package/dist/src/storage/sessionRepository.js +370 -0
- package/dist/src/storage/statsRepository.js +204 -0
- package/dist/src/transport/httpClient.js +126 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/messageMedia.js +285 -0
- package/dist/src/utils/modelCapabilities.js +108 -0
- package/dist/src/utils/modelDiscovery.js +170 -0
- package/dist/src/version.js +5 -0
- package/dist/src/workers/captureRetention.js +25 -0
- package/dist/src/workers/configWatcher.js +91 -0
- package/dist/src/workers/healthChecker.js +21 -0
- package/dist/src/workers/statsRotation.js +41 -0
- package/docs/LLM/output_schema.md +312 -0
- package/docs/benchmark.md +208 -0
- package/docs/mcp-guidelines.md +125 -0
- package/docs/mcp-service.md +178 -0
- package/docs/opencode.md +86 -0
- package/docs/providers.md +79 -0
- package/examples/benchmark.config.yaml +28 -0
- package/examples/providers/alibaba-dashscope.yaml +88 -0
- package/examples/providers/alibaba-llm.yaml +64 -0
- package/examples/providers/alibaba-registry.yaml +7 -0
- package/examples/providers/inference-v2-ray.yaml +29 -0
- package/examples/scenarios/assets/omni-call-sample.wav +0 -0
- package/examples/scenarios/custom.jsonl +5 -0
- package/examples/scenarios/custom.yaml +40 -0
- package/model-form-v2.png +0 -0
- package/package.json +66 -0
- package/provider-form-v2.png +0 -0
- package/provider-form.png +0 -0
- package/scripts/manual-test.sh +11 -0
- package/scripts/version-from-git.js +23 -0
- package/src/benchmark/artifacts.ts +149 -0
- package/src/benchmark/capabilityClassifier.ts +99 -0
- package/src/benchmark/capabilityStore.ts +174 -0
- package/src/benchmark/config.ts +337 -0
- package/src/benchmark/gates.ts +164 -0
- package/src/benchmark/jobs.ts +312 -0
- package/src/benchmark/runner.ts +2519 -0
- package/src/benchmark/schema.ts +443 -0
- package/src/benchmark/suites.ts +323 -0
- package/src/benchmark/tinyQaDataset.ts +428 -0
- package/src/benchmark/types.ts +442 -0
- package/src/config.ts +44 -0
- package/src/index.ts +195 -0
- package/src/mcp/client.ts +305 -0
- package/src/mcp/discovery.ts +266 -0
- package/src/mcp/policy.ts +105 -0
- package/src/mcp/registry.ts +164 -0
- package/src/mcp/service.ts +611 -0
- package/src/middleware/auth.ts +251 -0
- package/src/middleware/requestCapture.ts +245 -0
- package/src/middleware/requestStats.ts +163 -0
- package/src/pools/builder.ts +159 -0
- package/src/pools/repository.ts +71 -0
- package/src/pools/scheduler.ts +425 -0
- package/src/pools/types.ts +117 -0
- package/src/protocols/adapters/dashscope.ts +335 -0
- package/src/protocols/adapters/inferenceV2.ts +428 -0
- package/src/protocols/adapters/openai.ts +32 -0
- package/src/protocols/registry.ts +117 -0
- package/src/protocols/types.ts +81 -0
- package/src/providers/health.ts +207 -0
- package/src/providers/importer.ts +402 -0
- package/src/providers/modelRegistry.ts +415 -0
- package/src/providers/repository.ts +439 -0
- package/src/providers/types.ts +113 -0
- package/src/routes/admin.ts +666 -0
- package/src/routes/audio.ts +372 -0
- package/src/routes/chat.ts +301 -0
- package/src/routes/embeddings.ts +197 -0
- package/src/routes/images.ts +356 -0
- package/src/routes/mcp.ts +320 -0
- package/src/routes/mcpService.ts +114 -0
- package/src/routes/models.ts +50 -0
- package/src/routes/responses.ts +872 -0
- package/src/routes/sessions.ts +558 -0
- package/src/routes/stats.ts +312 -0
- package/src/routes/ui.ts +96 -0
- package/src/routes/videos.ts +132 -0
- package/src/routing/router.ts +501 -0
- package/src/services/imageGeneration.ts +396 -0
- package/src/services/imageUnderstanding.ts +449 -0
- package/src/services/videoGeneration.ts +127 -0
- package/src/storage/captureRepository.ts +1835 -0
- package/src/storage/files.ts +178 -0
- package/src/storage/imageCache.ts +405 -0
- package/src/storage/repositories.ts +494 -0
- package/src/storage/sessionRepository.ts +419 -0
- package/src/storage/statsRepository.ts +238 -0
- package/src/transport/httpClient.ts +145 -0
- package/src/types.ts +322 -0
- package/src/utils/messageMedia.ts +293 -0
- package/src/utils/modelCapabilities.ts +161 -0
- package/src/utils/modelDiscovery.ts +203 -0
- package/src/workers/captureRetention.ts +25 -0
- package/src/workers/configWatcher.ts +115 -0
- package/src/workers/healthChecker.ts +22 -0
- package/src/workers/statsRotation.ts +49 -0
- package/tests/benchmarkAdminRoutes.test.ts +82 -0
- package/tests/benchmarkBasics.test.ts +116 -0
- package/tests/captureAdminRoutes.test.ts +420 -0
- package/tests/captureRepository.test.ts +797 -0
- package/tests/cliLegacyRewrite.test.ts +45 -0
- package/tests/imageGeneration.service.test.ts +107 -0
- package/tests/imageUnderstanding.service.test.ts +123 -0
- package/tests/mcpPolicy.test.ts +105 -0
- package/tests/mcpService.test.ts +1245 -0
- package/tests/modelRef.test.ts +23 -0
- package/tests/modelsRoutes.test.ts +154 -0
- package/tests/sessionMediaCache.test.ts +167 -0
- package/tests/statsRoutes.test.ts +323 -0
- package/tsconfig.json +15 -0
- package/ui/index.html +16 -0
- package/ui/package-lock.json +8521 -0
- package/ui/package.json +52 -0
- package/ui/postcss.config.js +6 -0
- package/ui/public/assets/apple-touch-icon.png +0 -0
- package/ui/public/assets/favicon-16.png +0 -0
- package/ui/public/assets/favicon-32.png +0 -0
- package/ui/public/assets/icon-192.png +0 -0
- package/ui/public/assets/icon-512.png +0 -0
- package/ui/src/App.tsx +27 -0
- package/ui/src/api/client.ts +1503 -0
- package/ui/src/components/EndpointUsageGuide.tsx +361 -0
- package/ui/src/components/Layout.tsx +124 -0
- package/ui/src/components/MessageContent.tsx +365 -0
- package/ui/src/components/ToolCallMessage.tsx +179 -0
- package/ui/src/components/ToolPicker.tsx +442 -0
- package/ui/src/components/messageContentParser.test.ts +41 -0
- package/ui/src/components/messageContentParser.ts +73 -0
- package/ui/src/components/thinkingPreview.test.ts +27 -0
- package/ui/src/components/thinkingPreview.ts +15 -0
- package/ui/src/components/toMermaidSankey.test.ts +78 -0
- package/ui/src/components/toMermaidSankey.ts +56 -0
- package/ui/src/components/ui/button.tsx +58 -0
- package/ui/src/components/ui/input.tsx +21 -0
- package/ui/src/components/ui/textarea.tsx +21 -0
- package/ui/src/lib/utils.ts +6 -0
- package/ui/src/main.tsx +9 -0
- package/ui/src/pages/AgentPlayground.tsx +2010 -0
- package/ui/src/pages/Benchmark.tsx +988 -0
- package/ui/src/pages/Dashboard.tsx +581 -0
- package/ui/src/pages/Peek.tsx +962 -0
- package/ui/src/pages/Settings.tsx +2013 -0
- package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
- package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
- package/ui/src/pages/agentThinkingContent.test.ts +50 -0
- package/ui/src/pages/agentThinkingContent.ts +57 -0
- package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
- package/ui/src/pages/dashboardTokenUsage.ts +36 -0
- package/ui/src/pages/imageUpload.test.ts +39 -0
- package/ui/src/pages/imageUpload.ts +71 -0
- package/ui/src/pages/peekFilters.test.ts +29 -0
- package/ui/src/pages/peekFilters.ts +13 -0
- package/ui/src/pages/peekMedia.test.ts +58 -0
- package/ui/src/pages/peekMedia.ts +148 -0
- package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
- package/ui/src/pages/sessionAutoTitle.ts +106 -0
- package/ui/src/stores/settings.ts +58 -0
- package/ui/src/styles/globals.css +223 -0
- package/ui/src/vite-env.d.ts +8 -0
- package/ui/tailwind.config.js +106 -0
- package/ui/tsconfig.json +32 -0
- package/ui/vite.config.ts +37 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { McpServer, McpTool } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Client
|
|
5
|
+
*
|
|
6
|
+
* HTTP client for MCP servers using Streamable HTTP transport.
|
|
7
|
+
* Handles connection lifecycle, tool discovery, and tool execution.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface McpClientOptions {
|
|
11
|
+
url: string;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface McpJsonRpcRequest {
|
|
16
|
+
jsonrpc: "2.0";
|
|
17
|
+
id: number | string;
|
|
18
|
+
method: string;
|
|
19
|
+
params?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface McpJsonRpcResponse {
|
|
23
|
+
jsonrpc: "2.0";
|
|
24
|
+
id: number | string;
|
|
25
|
+
result?: unknown;
|
|
26
|
+
error?: {
|
|
27
|
+
code: number;
|
|
28
|
+
message: string;
|
|
29
|
+
data?: unknown;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface McpToolSchema {
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object";
|
|
38
|
+
properties?: Record<string, unknown>;
|
|
39
|
+
required?: string[];
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface McpToolsListResult {
|
|
44
|
+
tools: McpToolSchema[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface McpToolCallResult {
|
|
48
|
+
content: Array<{
|
|
49
|
+
type: "text" | "image" | "resource";
|
|
50
|
+
text?: string;
|
|
51
|
+
data?: string;
|
|
52
|
+
mimeType?: string;
|
|
53
|
+
}>;
|
|
54
|
+
isError?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class McpError extends Error {
|
|
58
|
+
constructor(
|
|
59
|
+
public code: number,
|
|
60
|
+
message: string,
|
|
61
|
+
public data?: unknown
|
|
62
|
+
) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "McpError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class McpClient {
|
|
69
|
+
private url: string;
|
|
70
|
+
private timeout: number;
|
|
71
|
+
private requestId = 0;
|
|
72
|
+
private sessionId: string | null = null;
|
|
73
|
+
|
|
74
|
+
constructor(options: McpClientOptions) {
|
|
75
|
+
this.url = options.url;
|
|
76
|
+
this.timeout = options.timeout ?? 30000;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Initialize the MCP connection.
|
|
81
|
+
*/
|
|
82
|
+
async initialize(): Promise<void> {
|
|
83
|
+
const response = await this.sendRequest("initialize", {
|
|
84
|
+
protocolVersion: "2024-11-05",
|
|
85
|
+
capabilities: {
|
|
86
|
+
tools: {},
|
|
87
|
+
},
|
|
88
|
+
clientInfo: {
|
|
89
|
+
name: "waypoi",
|
|
90
|
+
version: "0.2.0",
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Store session ID if provided
|
|
95
|
+
const initResponse = response as { _meta?: { sessionId?: string } };
|
|
96
|
+
if (initResponse._meta?.sessionId) {
|
|
97
|
+
this.sessionId = initResponse._meta.sessionId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Send initialized notification
|
|
101
|
+
await this.sendNotification("notifications/initialized");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* List available tools from the MCP server.
|
|
106
|
+
*/
|
|
107
|
+
async listTools(): Promise<McpToolSchema[]> {
|
|
108
|
+
const result = await this.sendRequest("tools/list", {});
|
|
109
|
+
const toolsResult = result as McpToolsListResult;
|
|
110
|
+
return toolsResult.tools ?? [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Call a tool on the MCP server.
|
|
115
|
+
*/
|
|
116
|
+
async callTool(name: string, args: Record<string, unknown>): Promise<McpToolCallResult> {
|
|
117
|
+
const result = await this.sendRequest("tools/call", {
|
|
118
|
+
name,
|
|
119
|
+
arguments: args,
|
|
120
|
+
});
|
|
121
|
+
return result as McpToolCallResult;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Close the MCP connection.
|
|
126
|
+
*/
|
|
127
|
+
async close(): Promise<void> {
|
|
128
|
+
try {
|
|
129
|
+
await this.sendNotification("notifications/cancelled", {
|
|
130
|
+
requestId: "session",
|
|
131
|
+
reason: "Client closing",
|
|
132
|
+
});
|
|
133
|
+
} catch {
|
|
134
|
+
// Ignore errors during close
|
|
135
|
+
}
|
|
136
|
+
this.sessionId = null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Send a JSON-RPC request and wait for response.
|
|
141
|
+
*/
|
|
142
|
+
private async sendRequest(method: string, params?: unknown): Promise<unknown> {
|
|
143
|
+
const id = ++this.requestId;
|
|
144
|
+
const request: McpJsonRpcRequest = {
|
|
145
|
+
jsonrpc: "2.0",
|
|
146
|
+
id,
|
|
147
|
+
method,
|
|
148
|
+
params,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
// MCP Streamable HTTP spec requires accepting both JSON and SSE
|
|
156
|
+
const headers: Record<string, string> = {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
"Accept": "application/json, text/event-stream",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (this.sessionId) {
|
|
162
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const response = await fetch(this.url, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers,
|
|
168
|
+
body: JSON.stringify(request),
|
|
169
|
+
signal: controller.signal,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new McpError(
|
|
174
|
+
-32000,
|
|
175
|
+
`HTTP error: ${response.status} ${response.statusText}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for session ID in response headers
|
|
180
|
+
const newSessionId = response.headers.get("Mcp-Session-Id");
|
|
181
|
+
if (newSessionId) {
|
|
182
|
+
this.sessionId = newSessionId;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Handle both JSON and SSE responses per MCP Streamable HTTP spec
|
|
186
|
+
const contentType = response.headers.get("Content-Type") ?? "";
|
|
187
|
+
|
|
188
|
+
if (contentType.includes("text/event-stream")) {
|
|
189
|
+
// Parse SSE response - collect events and find our response
|
|
190
|
+
return await this.parseSSEResponse(response, id);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const jsonResponse = await response.json() as McpJsonRpcResponse;
|
|
194
|
+
|
|
195
|
+
if (jsonResponse.error) {
|
|
196
|
+
throw new McpError(
|
|
197
|
+
jsonResponse.error.code,
|
|
198
|
+
jsonResponse.error.message,
|
|
199
|
+
jsonResponse.error.data
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return jsonResponse.result;
|
|
204
|
+
} finally {
|
|
205
|
+
clearTimeout(timeoutId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Parse Server-Sent Events response to extract JSON-RPC response.
|
|
211
|
+
* MCP Streamable HTTP can return SSE for streaming responses.
|
|
212
|
+
*/
|
|
213
|
+
private async parseSSEResponse(response: Response, expectedId: number | string): Promise<unknown> {
|
|
214
|
+
const text = await response.text();
|
|
215
|
+
const lines = text.split("\n");
|
|
216
|
+
|
|
217
|
+
let currentData = "";
|
|
218
|
+
|
|
219
|
+
for (const line of lines) {
|
|
220
|
+
if (line.startsWith("data:")) {
|
|
221
|
+
currentData += line.slice(5).trim();
|
|
222
|
+
} else if (line === "" && currentData) {
|
|
223
|
+
// End of event, try to parse
|
|
224
|
+
try {
|
|
225
|
+
const jsonResponse = JSON.parse(currentData) as McpJsonRpcResponse;
|
|
226
|
+
|
|
227
|
+
// Check if this is the response to our request
|
|
228
|
+
if (jsonResponse.id === expectedId) {
|
|
229
|
+
if (jsonResponse.error) {
|
|
230
|
+
throw new McpError(
|
|
231
|
+
jsonResponse.error.code,
|
|
232
|
+
jsonResponse.error.message,
|
|
233
|
+
jsonResponse.error.data
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
return jsonResponse.result;
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
if (e instanceof McpError) throw e;
|
|
240
|
+
// Ignore parsing errors, continue to next event
|
|
241
|
+
}
|
|
242
|
+
currentData = "";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Try parsing any remaining data
|
|
247
|
+
if (currentData) {
|
|
248
|
+
try {
|
|
249
|
+
const jsonResponse = JSON.parse(currentData) as McpJsonRpcResponse;
|
|
250
|
+
if (jsonResponse.id === expectedId) {
|
|
251
|
+
if (jsonResponse.error) {
|
|
252
|
+
throw new McpError(
|
|
253
|
+
jsonResponse.error.code,
|
|
254
|
+
jsonResponse.error.message,
|
|
255
|
+
jsonResponse.error.data
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return jsonResponse.result;
|
|
259
|
+
}
|
|
260
|
+
} catch (e) {
|
|
261
|
+
if (e instanceof McpError) throw e;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw new McpError(-32000, "No response found in SSE stream");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Send a JSON-RPC notification (no response expected).
|
|
270
|
+
*/
|
|
271
|
+
private async sendNotification(method: string, params?: unknown): Promise<void> {
|
|
272
|
+
const headers: Record<string, string> = {
|
|
273
|
+
"Content-Type": "application/json",
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (this.sessionId) {
|
|
277
|
+
headers["Mcp-Session-Id"] = this.sessionId;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const notification = {
|
|
281
|
+
jsonrpc: "2.0",
|
|
282
|
+
method,
|
|
283
|
+
params,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
await fetch(this.url, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers,
|
|
289
|
+
body: JSON.stringify(notification),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
get serverUrl(): string {
|
|
294
|
+
return this.url;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Create and initialize an MCP client.
|
|
300
|
+
*/
|
|
301
|
+
export async function createMcpClient(url: string): Promise<McpClient> {
|
|
302
|
+
const client = new McpClient({ url });
|
|
303
|
+
await client.initialize();
|
|
304
|
+
return client;
|
|
305
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { McpServer, McpTool } from "../types";
|
|
2
|
+
import { StoragePaths } from "../storage/files";
|
|
3
|
+
import { listMcpServers, updateMcpServerStatus } from "./registry";
|
|
4
|
+
import { McpClient, createMcpClient, McpToolSchema, McpError } from "./client";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* MCP Tool Discovery
|
|
8
|
+
*
|
|
9
|
+
* Connects to registered MCP servers and discovers available tools.
|
|
10
|
+
* Maintains an in-memory cache of tools and their source servers.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface DiscoveredTool extends McpTool {
|
|
14
|
+
serverId: string;
|
|
15
|
+
serverName: string;
|
|
16
|
+
serverUrl: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// In-memory cache of discovered tools
|
|
20
|
+
const toolsCache: Map<string, DiscoveredTool[]> = new Map();
|
|
21
|
+
|
|
22
|
+
// Active client connections
|
|
23
|
+
const activeClients: Map<string, McpClient> = new Map();
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Built-in server (the waypoi /mcp endpoint itself)
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/** Reserved ID for the built-in waypoi MCP server. */
|
|
30
|
+
export const BUILTIN_SERVER_ID = "builtin";
|
|
31
|
+
|
|
32
|
+
/** URL of the built-in MCP server (set once after app.listen). */
|
|
33
|
+
let builtinUrl: string | null = null;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Connect to the built-in /mcp endpoint and cache its tools.
|
|
37
|
+
* Called once in src/index.ts after app.listen().
|
|
38
|
+
*/
|
|
39
|
+
export async function discoverBuiltinTools(
|
|
40
|
+
paths: StoragePaths,
|
|
41
|
+
serverUrl: string
|
|
42
|
+
): Promise<DiscoveredTool[]> {
|
|
43
|
+
builtinUrl = serverUrl;
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const server: McpServer = {
|
|
46
|
+
id: BUILTIN_SERVER_ID,
|
|
47
|
+
name: "waypoi",
|
|
48
|
+
url: serverUrl,
|
|
49
|
+
enabled: true,
|
|
50
|
+
status: "unknown",
|
|
51
|
+
createdAt: now,
|
|
52
|
+
updatedAt: now,
|
|
53
|
+
};
|
|
54
|
+
return discoverServerTools(paths, server);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Return virtual server metadata for the built-in server.
|
|
59
|
+
* Returns null if discoverBuiltinTools hasn't been called yet.
|
|
60
|
+
*/
|
|
61
|
+
export function getBuiltinVirtualServer(): (McpServer & { connected: boolean }) | null {
|
|
62
|
+
if (!builtinUrl) return null;
|
|
63
|
+
const now = new Date();
|
|
64
|
+
const connected = isServerConnected(BUILTIN_SERVER_ID);
|
|
65
|
+
return {
|
|
66
|
+
id: BUILTIN_SERVER_ID,
|
|
67
|
+
name: "waypoi",
|
|
68
|
+
url: builtinUrl,
|
|
69
|
+
enabled: true,
|
|
70
|
+
status: connected ? "connected" : "error",
|
|
71
|
+
toolCount: getCachedToolsForServer(BUILTIN_SERVER_ID).length,
|
|
72
|
+
createdAt: now,
|
|
73
|
+
updatedAt: now,
|
|
74
|
+
connected,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Connect to a single MCP server and discover its tools.
|
|
80
|
+
*/
|
|
81
|
+
export async function discoverServerTools(
|
|
82
|
+
paths: StoragePaths,
|
|
83
|
+
server: McpServer
|
|
84
|
+
): Promise<DiscoveredTool[]> {
|
|
85
|
+
if (!server.enabled) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Create client and connect
|
|
91
|
+
const client = await createMcpClient(server.url);
|
|
92
|
+
activeClients.set(server.id, client);
|
|
93
|
+
|
|
94
|
+
// Discover tools
|
|
95
|
+
const toolSchemas = await client.listTools();
|
|
96
|
+
const tools: DiscoveredTool[] = toolSchemas.map((schema) => ({
|
|
97
|
+
name: schema.name,
|
|
98
|
+
description: schema.description,
|
|
99
|
+
inputSchema: schema.inputSchema,
|
|
100
|
+
serverId: server.id,
|
|
101
|
+
serverName: server.name,
|
|
102
|
+
serverUrl: server.url,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// Update server status
|
|
106
|
+
await updateMcpServerStatus(paths, server.id, "connected", tools.length);
|
|
107
|
+
|
|
108
|
+
// Cache tools
|
|
109
|
+
toolsCache.set(server.id, tools);
|
|
110
|
+
|
|
111
|
+
return tools;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const summary = summarizeMcpError(error);
|
|
114
|
+
console.error(`[waypoi] MCP discovery failed for ${server.name}: ${summary}`);
|
|
115
|
+
if (process.env.WAYPOI_DEBUG_ERRORS === "1") {
|
|
116
|
+
console.error(error);
|
|
117
|
+
}
|
|
118
|
+
await updateMcpServerStatus(paths, server.id, "error");
|
|
119
|
+
toolsCache.delete(server.id);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Discover tools from all enabled MCP servers.
|
|
126
|
+
*/
|
|
127
|
+
export async function discoverAllTools(paths: StoragePaths): Promise<DiscoveredTool[]> {
|
|
128
|
+
const servers = await listMcpServers(paths);
|
|
129
|
+
const enabledServers = servers.filter((s) => s.enabled);
|
|
130
|
+
|
|
131
|
+
// Discover tools from all servers in parallel
|
|
132
|
+
const results = await Promise.allSettled(
|
|
133
|
+
enabledServers.map((server) => discoverServerTools(paths, server))
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Collect all successful discoveries
|
|
137
|
+
const allTools: DiscoveredTool[] = [];
|
|
138
|
+
for (const result of results) {
|
|
139
|
+
if (result.status === "fulfilled") {
|
|
140
|
+
allTools.push(...result.value);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return allTools;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function summarizeMcpError(error: unknown): string {
|
|
148
|
+
if (error instanceof McpError) {
|
|
149
|
+
return error.message;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const err = error as { message?: string; code?: string; cause?: unknown };
|
|
153
|
+
if (err?.code) {
|
|
154
|
+
return err.code;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const cause = err?.cause as { code?: string; message?: string } | undefined;
|
|
158
|
+
if (cause?.code) {
|
|
159
|
+
return cause.code;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const message = err?.message;
|
|
163
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
164
|
+
return message;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return "unknown error";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get all cached tools (does not re-discover).
|
|
172
|
+
*/
|
|
173
|
+
export function getCachedTools(): DiscoveredTool[] {
|
|
174
|
+
const allTools: DiscoveredTool[] = [];
|
|
175
|
+
for (const tools of toolsCache.values()) {
|
|
176
|
+
allTools.push(...tools);
|
|
177
|
+
}
|
|
178
|
+
return allTools;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get tools for a specific server.
|
|
183
|
+
*/
|
|
184
|
+
export function getCachedToolsForServer(serverId: string): DiscoveredTool[] {
|
|
185
|
+
return toolsCache.get(serverId) ?? [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Find a tool by name across all servers.
|
|
190
|
+
*/
|
|
191
|
+
export function findTool(name: string): DiscoveredTool | undefined {
|
|
192
|
+
for (const tools of toolsCache.values()) {
|
|
193
|
+
const tool = tools.find((t) => t.name === name);
|
|
194
|
+
if (tool) return tool;
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Execute a tool on its source MCP server.
|
|
201
|
+
*/
|
|
202
|
+
export async function executeTool(
|
|
203
|
+
toolName: string,
|
|
204
|
+
args: Record<string, unknown>
|
|
205
|
+
): Promise<{ content: string; isError?: boolean }> {
|
|
206
|
+
const tool = findTool(toolName);
|
|
207
|
+
if (!tool) {
|
|
208
|
+
return { content: `Tool not found: ${toolName}`, isError: true };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const client = activeClients.get(tool.serverId);
|
|
212
|
+
if (!client) {
|
|
213
|
+
return { content: `Server not connected: ${tool.serverName}`, isError: true };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const result = await client.callTool(toolName, args);
|
|
218
|
+
|
|
219
|
+
// Extract text content from result
|
|
220
|
+
const textParts = result.content
|
|
221
|
+
.filter((c) => c.type === "text" && c.text)
|
|
222
|
+
.map((c) => c.text as string);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
content: textParts.join("\n") || "Tool executed successfully (no output)",
|
|
226
|
+
isError: result.isError,
|
|
227
|
+
};
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const errorMessage = error instanceof McpError
|
|
230
|
+
? error.message
|
|
231
|
+
: (error as Error).message;
|
|
232
|
+
return { content: `Tool execution failed: ${errorMessage}`, isError: true };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Disconnect from a specific server.
|
|
238
|
+
*/
|
|
239
|
+
export async function disconnectServer(serverId: string): Promise<void> {
|
|
240
|
+
const client = activeClients.get(serverId);
|
|
241
|
+
if (client) {
|
|
242
|
+
await client.close();
|
|
243
|
+
activeClients.delete(serverId);
|
|
244
|
+
}
|
|
245
|
+
toolsCache.delete(serverId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Disconnect from all servers.
|
|
250
|
+
*/
|
|
251
|
+
export async function disconnectAllServers(): Promise<void> {
|
|
252
|
+
const closePromises: Promise<void>[] = [];
|
|
253
|
+
for (const [serverId, client] of activeClients) {
|
|
254
|
+
closePromises.push(client.close());
|
|
255
|
+
activeClients.delete(serverId);
|
|
256
|
+
toolsCache.delete(serverId);
|
|
257
|
+
}
|
|
258
|
+
await Promise.allSettled(closePromises);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Check if a server is currently connected.
|
|
263
|
+
*/
|
|
264
|
+
export function isServerConnected(serverId: string): boolean {
|
|
265
|
+
return activeClients.has(serverId);
|
|
266
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
export type McpTypedError = Error & { type: string };
|
|
4
|
+
|
|
5
|
+
export interface BinaryOutputPolicyInput {
|
|
6
|
+
include_data?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface BinaryOutputPolicyResolved {
|
|
10
|
+
outputDir: string;
|
|
11
|
+
includeData: boolean;
|
|
12
|
+
outputBaseRoot: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ResolveBinaryOutputPolicyOptions {
|
|
16
|
+
env?: NodeJS.ProcessEnv;
|
|
17
|
+
defaultBaseDir?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function typedError(type: string, message: string): McpTypedError {
|
|
21
|
+
const error = new Error(message) as McpTypedError;
|
|
22
|
+
error.type = type;
|
|
23
|
+
return error;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveBinaryOutputPolicy(
|
|
27
|
+
input: BinaryOutputPolicyInput,
|
|
28
|
+
options: ResolveBinaryOutputPolicyOptions = {}
|
|
29
|
+
): BinaryOutputPolicyResolved {
|
|
30
|
+
const env = options.env ?? process.env;
|
|
31
|
+
const defaultBaseDir = options.defaultBaseDir ?? process.cwd();
|
|
32
|
+
const strict = parseBooleanEnv(env.WAYPOI_MCP_STRICT_OUTPUT_ROOT);
|
|
33
|
+
const configuredRoot = env.WAYPOI_MCP_OUTPUT_ROOT?.trim();
|
|
34
|
+
|
|
35
|
+
if (strict && !configuredRoot) {
|
|
36
|
+
throw typedError(
|
|
37
|
+
"invalid_request",
|
|
38
|
+
"WAYPOI_MCP_STRICT_OUTPUT_ROOT=true requires WAYPOI_MCP_OUTPUT_ROOT to be set."
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let baseRoot = defaultBaseDir;
|
|
43
|
+
if (configuredRoot) {
|
|
44
|
+
if (!path.isAbsolute(configuredRoot)) {
|
|
45
|
+
if (strict) {
|
|
46
|
+
throw typedError(
|
|
47
|
+
"invalid_request",
|
|
48
|
+
`WAYPOI_MCP_OUTPUT_ROOT must be an absolute path, got '${configuredRoot}'.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
baseRoot = path.resolve(configuredRoot);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const configuredSubdir = env.WAYPOI_MCP_OUTPUT_SUBDIR?.trim();
|
|
57
|
+
if (configuredSubdir && path.isAbsolute(configuredSubdir)) {
|
|
58
|
+
throw typedError(
|
|
59
|
+
"invalid_request",
|
|
60
|
+
`WAYPOI_MCP_OUTPUT_SUBDIR must be relative, got '${configuredSubdir}'.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const outputDir = configuredSubdir
|
|
64
|
+
? path.resolve(baseRoot, configuredSubdir)
|
|
65
|
+
: path.join(baseRoot, "generated-images");
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
outputDir,
|
|
69
|
+
includeData: input.include_data ?? false,
|
|
70
|
+
outputBaseRoot: baseRoot,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseBooleanEnv(value: string | undefined): boolean {
|
|
75
|
+
if (!value) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return value === "1" || value.toLowerCase() === "true";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function validateSingleImageInput(input: {
|
|
82
|
+
image_path?: string;
|
|
83
|
+
image_url?: string;
|
|
84
|
+
}): void {
|
|
85
|
+
const hasPath = Boolean(input.image_path);
|
|
86
|
+
const hasUrl = Boolean(input.image_url);
|
|
87
|
+
if ((hasPath && hasUrl) || (!hasPath && !hasUrl)) {
|
|
88
|
+
throw typedError(
|
|
89
|
+
"invalid_request",
|
|
90
|
+
"Exactly one image source is required: provide either image_path or image_url."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function validateAtMostOneImageInput(input: {
|
|
96
|
+
image_path?: string;
|
|
97
|
+
image_url?: string;
|
|
98
|
+
}): void {
|
|
99
|
+
if (input.image_path && input.image_url) {
|
|
100
|
+
throw typedError(
|
|
101
|
+
"invalid_request",
|
|
102
|
+
"Provide either image_path or image_url, not both."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|