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.
Files changed (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,1245 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import path from "path";
4
+ import { promises as fs } from "fs";
5
+ import Fastify from "fastify";
6
+ import { registerMcpServiceRoutes } from "../src/routes/mcpService";
7
+ import { StoragePaths } from "../src/storage/files";
8
+
9
+ const MCP_HEADERS = {
10
+ host: "localhost:8011",
11
+ accept: "application/json, text/event-stream",
12
+ "content-type": "application/json",
13
+ };
14
+
15
+ async function makeWorkspaceTempDir(prefix: string): Promise<string> {
16
+ const base = path.join(process.cwd(), "tmp");
17
+ await fs.mkdir(base, { recursive: true });
18
+ return fs.mkdtemp(path.join(base, prefix));
19
+ }
20
+
21
+ function makePaths(baseDir: string): StoragePaths {
22
+ return {
23
+ baseDir,
24
+ configPath: path.join(baseDir, "config.yaml"),
25
+ healthPath: path.join(baseDir, "health.json"),
26
+ providerHealthPath: path.join(baseDir, "providers_health.json"),
27
+ requestLogPath: path.join(baseDir, "request_logs.jsonl"),
28
+ providersPath: path.join(baseDir, "providers.json"),
29
+ poolsPath: path.join(baseDir, "pools.json"),
30
+ poolStatePath: path.join(baseDir, "pool_state.json"),
31
+ };
32
+ }
33
+
34
+ function withTemporaryEnv(
35
+ patch: Partial<NodeJS.ProcessEnv>,
36
+ run: () => Promise<void>
37
+ ): Promise<void> {
38
+ const keys = Object.keys(patch) as Array<keyof NodeJS.ProcessEnv>;
39
+ const previous = new Map<keyof NodeJS.ProcessEnv, string | undefined>();
40
+ for (const key of keys) {
41
+ previous.set(key, process.env[key]);
42
+ const value = patch[key];
43
+ if (value === undefined) {
44
+ delete process.env[key];
45
+ } else {
46
+ process.env[key] = value;
47
+ }
48
+ }
49
+ return run().finally(() => {
50
+ for (const key of keys) {
51
+ const value = previous.get(key);
52
+ if (value === undefined) {
53
+ delete process.env[key];
54
+ } else {
55
+ process.env[key] = value;
56
+ }
57
+ }
58
+ });
59
+ }
60
+
61
+ test("mcp /mcp initialize, list tools, and call generate_image", async () => {
62
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
63
+ const app = Fastify();
64
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
65
+ runImageGeneration: async () => ({
66
+ model: "mock/diffusion",
67
+ statusCode: 200,
68
+ headers: { "content-type": "application/json" },
69
+ payload: { created: 1730000000, data: [{ b64_json: "AAA" }] },
70
+ route: {
71
+ endpointId: "ep-1",
72
+ endpointName: "mock",
73
+ upstreamModel: "upstream",
74
+ },
75
+ }),
76
+ normalizeImageGenerationPayload: async (_paths, payload, model) => ({
77
+ model,
78
+ created: (payload as { created: number }).created,
79
+ images: [{ index: 0, url: "data:image/png;base64,AAA", b64_json: "AAA" }],
80
+ }),
81
+ });
82
+
83
+ const init = await app.inject({
84
+ method: "POST",
85
+ url: "/mcp",
86
+ headers: MCP_HEADERS,
87
+ payload: {
88
+ jsonrpc: "2.0",
89
+ id: 1,
90
+ method: "initialize",
91
+ params: {
92
+ protocolVersion: "2025-06-18",
93
+ capabilities: {},
94
+ clientInfo: { name: "test", version: "1.0.0" },
95
+ },
96
+ },
97
+ });
98
+ assert.equal(init.statusCode, 200);
99
+ const sessionId = init.headers["mcp-session-id"];
100
+ assert.ok(typeof sessionId === "string" && sessionId.length > 0);
101
+
102
+ await app.inject({
103
+ method: "POST",
104
+ url: "/mcp",
105
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
106
+ payload: {
107
+ jsonrpc: "2.0",
108
+ method: "notifications/initialized",
109
+ params: {},
110
+ },
111
+ });
112
+
113
+ const listTools = await app.inject({
114
+ method: "POST",
115
+ url: "/mcp",
116
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
117
+ payload: {
118
+ jsonrpc: "2.0",
119
+ id: 2,
120
+ method: "tools/list",
121
+ params: {},
122
+ },
123
+ });
124
+ assert.equal(listTools.statusCode, 200);
125
+ const listJson = listTools.json() as {
126
+ result?: { tools?: Array<{ name: string; description?: string }> };
127
+ };
128
+ const generateImageTool = listJson.result?.tools?.find((tool) => tool.name === "generate_image");
129
+ assert.ok(generateImageTool);
130
+ const understandImageTool = listJson.result?.tools?.find((tool) => tool.name === "understand_image");
131
+ assert.ok(understandImageTool);
132
+ const desc = generateImageTool?.description ?? "";
133
+ assert.match(desc, /generated-images by default/);
134
+ assert.match(desc, /WAYPOI_MCP_OUTPUT_ROOT/);
135
+ assert.match(desc, /file_path or file_paths/);
136
+ const understandDesc = understandImageTool?.description ?? "";
137
+ assert.match(understandDesc, /Provide exactly one of image_path or image_url/);
138
+ assert.match(understandDesc, /original-image pixel coordinates/);
139
+
140
+ const call = await app.inject({
141
+ method: "POST",
142
+ url: "/mcp",
143
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
144
+ payload: {
145
+ jsonrpc: "2.0",
146
+ id: 3,
147
+ method: "tools/call",
148
+ params: {
149
+ name: "generate_image",
150
+ arguments: {
151
+ prompt: "sunset over mountains",
152
+ },
153
+ },
154
+ },
155
+ });
156
+ assert.equal(call.statusCode, 200);
157
+ const callJson = call.json() as {
158
+ result?: { content?: Array<{ type: string; text?: string }> };
159
+ };
160
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "{}";
161
+ const payload = JSON.parse(text) as { ok: boolean; file_path: string; model: string };
162
+ assert.equal(payload.ok, true);
163
+ assert.equal(payload.model, "mock/diffusion");
164
+ assert.match(payload.file_path, /^generated-images[/\\]image-1730000000-0\.png$/);
165
+
166
+ await app.close();
167
+ });
168
+
169
+ test("mcp tool returns typed no_diffusion_model error output", async () => {
170
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
171
+ const app = Fastify();
172
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
173
+ runImageGeneration: async () => {
174
+ const err = new Error("No diffusion model available");
175
+ (err as Error & { type: string }).type = "no_diffusion_model";
176
+ throw err;
177
+ },
178
+ normalizeImageGenerationPayload: async () => {
179
+ throw new Error("should not run");
180
+ },
181
+ });
182
+
183
+ const init = await app.inject({
184
+ method: "POST",
185
+ url: "/mcp",
186
+ headers: MCP_HEADERS,
187
+ payload: {
188
+ jsonrpc: "2.0",
189
+ id: 1,
190
+ method: "initialize",
191
+ params: {
192
+ protocolVersion: "2025-06-18",
193
+ capabilities: {},
194
+ clientInfo: { name: "test", version: "1.0.0" },
195
+ },
196
+ },
197
+ });
198
+ const sessionId = init.headers["mcp-session-id"] as string;
199
+ await app.inject({
200
+ method: "POST",
201
+ url: "/mcp",
202
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
203
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
204
+ });
205
+
206
+ const call = await app.inject({
207
+ method: "POST",
208
+ url: "/mcp",
209
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
210
+ payload: {
211
+ jsonrpc: "2.0",
212
+ id: 3,
213
+ method: "tools/call",
214
+ params: {
215
+ name: "generate_image",
216
+ arguments: { prompt: "any" },
217
+ },
218
+ },
219
+ });
220
+ const callJson = call.json() as {
221
+ result?: { content?: Array<{ type: string; text?: string }>; isError?: boolean };
222
+ };
223
+ assert.equal(callJson.result?.isError, true);
224
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "";
225
+ assert.match(text, /"type":"no_diffusion_model"/);
226
+
227
+ await app.close();
228
+ });
229
+
230
+ test("mcp localhost guard blocks non-local hosts", async () => {
231
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
232
+ const app = Fastify();
233
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
234
+ runImageGeneration: async () => {
235
+ throw new Error("unused");
236
+ },
237
+ normalizeImageGenerationPayload: async () => {
238
+ throw new Error("unused");
239
+ },
240
+ });
241
+
242
+ const res = await app.inject({
243
+ method: "POST",
244
+ url: "/mcp",
245
+ headers: { host: "evil.example.com:8000" },
246
+ payload: {
247
+ jsonrpc: "2.0",
248
+ id: 1,
249
+ method: "initialize",
250
+ params: {
251
+ protocolVersion: "2025-06-18",
252
+ capabilities: {},
253
+ clientInfo: { name: "test", version: "1.0.0" },
254
+ },
255
+ },
256
+ });
257
+ assert.equal(res.statusCode, 403);
258
+ await app.close();
259
+ });
260
+
261
+ test("mcp generate_image writes to default config dir", async () => {
262
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
263
+ const app = Fastify();
264
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
265
+ runImageGeneration: async () => ({
266
+ model: "mock/diffusion",
267
+ statusCode: 200,
268
+ headers: { "content-type": "application/json" },
269
+ payload: { created: 1730000000, data: [{ b64_json: "AQID" }] },
270
+ route: {
271
+ endpointId: "ep-1",
272
+ endpointName: "mock",
273
+ upstreamModel: "upstream",
274
+ },
275
+ }),
276
+ normalizeImageGenerationPayload: async (_paths, payload, model) => ({
277
+ model,
278
+ created: (payload as { created: number }).created,
279
+ images: [{ index: 0, url: "data:image/png;base64,AQID", b64_json: "AQID" }],
280
+ }),
281
+ });
282
+
283
+ const init = await app.inject({
284
+ method: "POST",
285
+ url: "/mcp",
286
+ headers: MCP_HEADERS,
287
+ payload: {
288
+ jsonrpc: "2.0",
289
+ id: 1,
290
+ method: "initialize",
291
+ params: {
292
+ protocolVersion: "2025-06-18",
293
+ capabilities: {},
294
+ clientInfo: { name: "test", version: "1.0.0" },
295
+ },
296
+ },
297
+ });
298
+ const sessionId = init.headers["mcp-session-id"] as string;
299
+ await app.inject({
300
+ method: "POST",
301
+ url: "/mcp",
302
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
303
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
304
+ });
305
+
306
+ const call = await app.inject({
307
+ method: "POST",
308
+ url: "/mcp",
309
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
310
+ payload: {
311
+ jsonrpc: "2.0",
312
+ id: 3,
313
+ method: "tools/call",
314
+ params: {
315
+ name: "generate_image",
316
+ arguments: { prompt: "any" },
317
+ },
318
+ },
319
+ });
320
+ assert.equal(call.statusCode, 200);
321
+ const callJson = call.json() as {
322
+ result?: { content?: Array<{ type: string; text?: string }> };
323
+ };
324
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "{}";
325
+ const payload = JSON.parse(text) as { ok: boolean; file_path: string };
326
+ assert.equal(payload.ok, true);
327
+ assert.match(payload.file_path, /^generated-images[/\\]image-1730000000-0\.png$/);
328
+ // verify the file was written under baseDir
329
+ const written = await fs.readFile(path.join(baseDir, payload.file_path));
330
+ assert.equal(written.length, 3);
331
+
332
+ await app.close();
333
+ });
334
+
335
+ test("mcp generate_image can include inline data when include_data=true with file output", async () => {
336
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
337
+ const app = Fastify();
338
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
339
+ runImageGeneration: async () => ({
340
+ model: "mock/diffusion",
341
+ statusCode: 200,
342
+ headers: { "content-type": "application/json" },
343
+ payload: { created: 1730000000, data: [{ b64_json: "AQID", url: "data:image/png;base64,AQID" }] },
344
+ route: {
345
+ endpointId: "ep-1",
346
+ endpointName: "mock",
347
+ upstreamModel: "upstream",
348
+ },
349
+ }),
350
+ normalizeImageGenerationPayload: async () => ({
351
+ model: "mock/diffusion",
352
+ created: 1730000000,
353
+ images: [{ index: 0, b64_json: "AQID", url: "data:image/png;base64,AQID" }],
354
+ }),
355
+ });
356
+
357
+ const init = await app.inject({
358
+ method: "POST",
359
+ url: "/mcp",
360
+ headers: MCP_HEADERS,
361
+ payload: {
362
+ jsonrpc: "2.0",
363
+ id: 1,
364
+ method: "initialize",
365
+ params: {
366
+ protocolVersion: "2025-06-18",
367
+ capabilities: {},
368
+ clientInfo: { name: "test", version: "1.0.0" },
369
+ },
370
+ },
371
+ });
372
+ const sessionId = init.headers["mcp-session-id"] as string;
373
+ await app.inject({
374
+ method: "POST",
375
+ url: "/mcp",
376
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
377
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
378
+ });
379
+
380
+ const call = await app.inject({
381
+ method: "POST",
382
+ url: "/mcp",
383
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
384
+ payload: {
385
+ jsonrpc: "2.0",
386
+ id: 3,
387
+ method: "tools/call",
388
+ params: {
389
+ name: "generate_image",
390
+ arguments: {
391
+ prompt: "any",
392
+ include_data: true,
393
+ },
394
+ },
395
+ },
396
+ });
397
+ assert.equal(call.statusCode, 200);
398
+ const callJson = call.json() as {
399
+ result?: {
400
+ content?: Array<{ type: string; text?: string }>;
401
+ structuredContent?: {
402
+ ok: boolean;
403
+ artifacts: Array<{ file_path: string; b64_json?: string; url?: string }>;
404
+ };
405
+ };
406
+ };
407
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "{}";
408
+ const payload = JSON.parse(text) as { ok: boolean; file_path: string };
409
+ assert.equal(payload.ok, true);
410
+ assert.equal(payload.file_path, "generated-images/image-1730000000-0.png");
411
+ assert.doesNotMatch(text, /AQID/);
412
+ assert.equal(typeof callJson.result?.structuredContent?.artifacts[0]?.b64_json, "string");
413
+ assert.equal(typeof callJson.result?.structuredContent?.artifacts[0]?.url, "string");
414
+
415
+ await app.close();
416
+ });
417
+
418
+ test("mcp generate_image succeeds without workspace_root", async () => {
419
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
420
+ const app = Fastify();
421
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
422
+ runImageGeneration: async () => ({
423
+ model: "mock/diffusion",
424
+ statusCode: 200,
425
+ headers: { "content-type": "application/json" },
426
+ payload: { created: 1730000000, data: [{ b64_json: "AQID" }] },
427
+ route: {
428
+ endpointId: "ep-1",
429
+ endpointName: "mock",
430
+ upstreamModel: "upstream",
431
+ },
432
+ }),
433
+ normalizeImageGenerationPayload: async (_paths, payload, model) => ({
434
+ model,
435
+ created: (payload as { created: number }).created,
436
+ images: [{ index: 0, url: "data:image/png;base64,AQID", b64_json: "AQID" }],
437
+ }),
438
+ });
439
+
440
+ const init = await app.inject({
441
+ method: "POST",
442
+ url: "/mcp",
443
+ headers: MCP_HEADERS,
444
+ payload: {
445
+ jsonrpc: "2.0",
446
+ id: 1,
447
+ method: "initialize",
448
+ params: {
449
+ protocolVersion: "2025-06-18",
450
+ capabilities: {},
451
+ clientInfo: { name: "test", version: "1.0.0" },
452
+ },
453
+ },
454
+ });
455
+ const sessionId = init.headers["mcp-session-id"] as string;
456
+ await app.inject({
457
+ method: "POST",
458
+ url: "/mcp",
459
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
460
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
461
+ });
462
+
463
+ const call = await app.inject({
464
+ method: "POST",
465
+ url: "/mcp",
466
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
467
+ payload: {
468
+ jsonrpc: "2.0",
469
+ id: 3,
470
+ method: "tools/call",
471
+ params: {
472
+ name: "generate_image",
473
+ arguments: {
474
+ prompt: "any",
475
+ },
476
+ },
477
+ },
478
+ });
479
+ assert.equal(call.statusCode, 200);
480
+ const callJson = call.json() as {
481
+ result?: { content?: Array<{ type: string; text?: string }>; isError?: boolean };
482
+ };
483
+ assert.equal(callJson.result?.isError, undefined);
484
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "";
485
+ assert.match(text, /"ok":true/);
486
+
487
+ await app.close();
488
+ });
489
+
490
+ test("mcp generate_image defaults to baseDir/generated-images when output target is omitted", async () => {
491
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
492
+ const app = Fastify();
493
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
494
+ runImageGeneration: async () => ({
495
+ model: "mock/diffusion",
496
+ statusCode: 200,
497
+ headers: { "content-type": "application/json" },
498
+ payload: { created: 1730000000, data: [{ b64_json: "AQID" }] },
499
+ route: {
500
+ endpointId: "ep-1",
501
+ endpointName: "mock",
502
+ upstreamModel: "upstream",
503
+ },
504
+ }),
505
+ normalizeImageGenerationPayload: async () => ({
506
+ model: "mock/diffusion",
507
+ created: 1730000000,
508
+ images: [{ index: 0, b64_json: "AQID" }],
509
+ }),
510
+ });
511
+
512
+ const init = await app.inject({
513
+ method: "POST",
514
+ url: "/mcp",
515
+ headers: MCP_HEADERS,
516
+ payload: {
517
+ jsonrpc: "2.0",
518
+ id: 1,
519
+ method: "initialize",
520
+ params: {
521
+ protocolVersion: "2025-06-18",
522
+ capabilities: {},
523
+ clientInfo: { name: "test", version: "1.0.0" },
524
+ },
525
+ },
526
+ });
527
+ const sessionId = init.headers["mcp-session-id"] as string;
528
+ await app.inject({
529
+ method: "POST",
530
+ url: "/mcp",
531
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
532
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
533
+ });
534
+
535
+ const call = await app.inject({
536
+ method: "POST",
537
+ url: "/mcp",
538
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
539
+ payload: {
540
+ jsonrpc: "2.0",
541
+ id: 3,
542
+ method: "tools/call",
543
+ params: {
544
+ name: "generate_image",
545
+ arguments: {
546
+ prompt: "any",
547
+ },
548
+ },
549
+ },
550
+ });
551
+ assert.equal(call.statusCode, 200);
552
+ const callJson = call.json() as {
553
+ result?: { content?: Array<{ type: string; text?: string }> };
554
+ };
555
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "{}";
556
+ const payload = JSON.parse(text) as { ok: boolean; file_path: string };
557
+ assert.equal(payload.ok, true);
558
+ assert.match(payload.file_path, /^generated-images[/\\]image-1730000000-0\.png$/);
559
+ const written = await fs.readFile(path.join(baseDir, payload.file_path));
560
+ assert.equal(written.length, 3);
561
+
562
+ await app.close();
563
+ });
564
+
565
+ test("mcp generate_image resolves WAYPOI_MCP_OUTPUT_ROOT and WAYPOI_MCP_OUTPUT_SUBDIR", async () => {
566
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
567
+ const pinnedRoot = path.join(baseDir, "manga");
568
+ const pinnedWorkDir = path.join(pinnedRoot, "work");
569
+ await fs.mkdir(pinnedWorkDir, { recursive: true });
570
+
571
+ await withTemporaryEnv(
572
+ {
573
+ WAYPOI_MCP_OUTPUT_ROOT: pinnedRoot,
574
+ WAYPOI_MCP_OUTPUT_SUBDIR: "work",
575
+ },
576
+ async () => {
577
+ const app = Fastify();
578
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
579
+ runImageGeneration: async () => ({
580
+ model: "mock/diffusion",
581
+ statusCode: 200,
582
+ headers: { "content-type": "application/json" },
583
+ payload: { created: 1730000000, data: [{ b64_json: "AQID" }] },
584
+ route: {
585
+ endpointId: "ep-1",
586
+ endpointName: "mock",
587
+ upstreamModel: "upstream",
588
+ },
589
+ }),
590
+ normalizeImageGenerationPayload: async () => ({
591
+ model: "mock/diffusion",
592
+ created: 1730000000,
593
+ images: [{ index: 0, b64_json: "AQID" }],
594
+ }),
595
+ });
596
+
597
+ const init = await app.inject({
598
+ method: "POST",
599
+ url: "/mcp",
600
+ headers: MCP_HEADERS,
601
+ payload: {
602
+ jsonrpc: "2.0",
603
+ id: 1,
604
+ method: "initialize",
605
+ params: {
606
+ protocolVersion: "2025-06-18",
607
+ capabilities: {},
608
+ clientInfo: { name: "test", version: "1.0.0" },
609
+ },
610
+ },
611
+ });
612
+ const sessionId = init.headers["mcp-session-id"] as string;
613
+ await app.inject({
614
+ method: "POST",
615
+ url: "/mcp",
616
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
617
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
618
+ });
619
+
620
+ const call = await app.inject({
621
+ method: "POST",
622
+ url: "/mcp",
623
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
624
+ payload: {
625
+ jsonrpc: "2.0",
626
+ id: 3,
627
+ method: "tools/call",
628
+ params: {
629
+ name: "generate_image",
630
+ arguments: { prompt: "any" },
631
+ },
632
+ },
633
+ });
634
+ assert.equal(call.statusCode, 200);
635
+ const callJson = call.json() as {
636
+ result?: { content?: Array<{ type: string; text?: string }> };
637
+ };
638
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "{}";
639
+ const payload = JSON.parse(text) as { ok: boolean; file_path: string };
640
+ assert.equal(payload.ok, true);
641
+ assert.match(payload.file_path, /^work[/\\]image-1730000000-0\.png$/);
642
+ const written = await fs.readFile(path.join(pinnedRoot, payload.file_path));
643
+ assert.equal(written.length, 3);
644
+
645
+ await app.close();
646
+ }
647
+ );
648
+ });
649
+
650
+ test("mcp generate_image forces b64_json when writing files", async () => {
651
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
652
+ let observedResponseFormat: string | undefined;
653
+ const app = Fastify();
654
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
655
+ runImageGeneration: async (_paths, request) => {
656
+ observedResponseFormat = request.response_format;
657
+ return {
658
+ model: "mock/diffusion",
659
+ statusCode: 200,
660
+ headers: { "content-type": "application/json" },
661
+ payload: { created: 1730000000, data: [{ b64_json: "AQID" }] },
662
+ route: {
663
+ endpointId: "ep-1",
664
+ endpointName: "mock",
665
+ upstreamModel: "upstream",
666
+ },
667
+ };
668
+ },
669
+ normalizeImageGenerationPayload: async () => ({
670
+ model: "mock/diffusion",
671
+ created: 1730000000,
672
+ images: [{ index: 0, b64_json: "AQID" }],
673
+ }),
674
+ });
675
+
676
+ const init = await app.inject({
677
+ method: "POST",
678
+ url: "/mcp",
679
+ headers: MCP_HEADERS,
680
+ payload: {
681
+ jsonrpc: "2.0",
682
+ id: 1,
683
+ method: "initialize",
684
+ params: {
685
+ protocolVersion: "2025-06-18",
686
+ capabilities: {},
687
+ clientInfo: { name: "test", version: "1.0.0" },
688
+ },
689
+ },
690
+ });
691
+ const sessionId = init.headers["mcp-session-id"] as string;
692
+ await app.inject({
693
+ method: "POST",
694
+ url: "/mcp",
695
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
696
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
697
+ });
698
+
699
+ const call = await app.inject({
700
+ method: "POST",
701
+ url: "/mcp",
702
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
703
+ payload: {
704
+ jsonrpc: "2.0",
705
+ id: 3,
706
+ method: "tools/call",
707
+ params: {
708
+ name: "generate_image",
709
+ arguments: {
710
+ prompt: "any",
711
+ response_format: "url",
712
+ },
713
+ },
714
+ },
715
+ });
716
+ assert.equal(call.statusCode, 200);
717
+ assert.equal(observedResponseFormat, "b64_json");
718
+
719
+ await app.close();
720
+ });
721
+
722
+ test("mcp generate_image accepts image_url for edit-style generation", async () => {
723
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
724
+ let observedImageUrl: string | undefined;
725
+ const app = Fastify();
726
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
727
+ runImageGeneration: async (_paths, request) => {
728
+ observedImageUrl = request.image_url;
729
+ return {
730
+ model: "mock/diffusion",
731
+ statusCode: 200,
732
+ headers: { "content-type": "application/json" },
733
+ payload: { created: 1730000000, data: [{ b64_json: "AQID" }] },
734
+ route: {
735
+ endpointId: "ep-1",
736
+ endpointName: "mock",
737
+ upstreamModel: "upstream",
738
+ },
739
+ };
740
+ },
741
+ normalizeImageGenerationPayload: async () => ({
742
+ model: "mock/diffusion",
743
+ created: 1730000000,
744
+ images: [{ index: 0, b64_json: "AQID", url: "data:image/png;base64,AQID" }],
745
+ }),
746
+ });
747
+
748
+ const init = await app.inject({
749
+ method: "POST",
750
+ url: "/mcp",
751
+ headers: MCP_HEADERS,
752
+ payload: {
753
+ jsonrpc: "2.0",
754
+ id: 1,
755
+ method: "initialize",
756
+ params: {
757
+ protocolVersion: "2025-06-18",
758
+ capabilities: {},
759
+ clientInfo: { name: "test", version: "1.0.0" },
760
+ },
761
+ },
762
+ });
763
+ const sessionId = init.headers["mcp-session-id"] as string;
764
+ await app.inject({
765
+ method: "POST",
766
+ url: "/mcp",
767
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
768
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
769
+ });
770
+
771
+ const call = await app.inject({
772
+ method: "POST",
773
+ url: "/mcp",
774
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
775
+ payload: {
776
+ jsonrpc: "2.0",
777
+ id: 3,
778
+ method: "tools/call",
779
+ params: {
780
+ name: "generate_image",
781
+ arguments: {
782
+ prompt: "edit",
783
+ image_url: "data:image/png;base64,AQID",
784
+ },
785
+ },
786
+ },
787
+ });
788
+ assert.equal(call.statusCode, 200);
789
+ assert.equal(observedImageUrl, "data:image/png;base64,AQID");
790
+ await app.close();
791
+ });
792
+
793
+ test("mcp generate_image accepts image_path for edit-style generation", async () => {
794
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
795
+ const inputPath = path.join(baseDir, "input.png");
796
+ const onePixelPngBase64 =
797
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7Zk3cAAAAASUVORK5CYII=";
798
+ await fs.writeFile(inputPath, Buffer.from(onePixelPngBase64, "base64"));
799
+
800
+ let observedImageUrl: string | undefined;
801
+ const app = Fastify();
802
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
803
+ runImageGeneration: async (_paths, request) => {
804
+ observedImageUrl = request.image_url;
805
+ return {
806
+ model: "mock/diffusion",
807
+ statusCode: 200,
808
+ headers: { "content-type": "application/json" },
809
+ payload: { created: 1730000000, data: [{ b64_json: "AQID" }] },
810
+ route: {
811
+ endpointId: "ep-1",
812
+ endpointName: "mock",
813
+ upstreamModel: "upstream",
814
+ },
815
+ };
816
+ },
817
+ normalizeImageGenerationPayload: async () => ({
818
+ model: "mock/diffusion",
819
+ created: 1730000000,
820
+ images: [{ index: 0, b64_json: "AQID", url: "data:image/png;base64,AQID" }],
821
+ }),
822
+ });
823
+
824
+ const init = await app.inject({
825
+ method: "POST",
826
+ url: "/mcp",
827
+ headers: MCP_HEADERS,
828
+ payload: {
829
+ jsonrpc: "2.0",
830
+ id: 1,
831
+ method: "initialize",
832
+ params: {
833
+ protocolVersion: "2025-06-18",
834
+ capabilities: {},
835
+ clientInfo: { name: "test", version: "1.0.0" },
836
+ },
837
+ },
838
+ });
839
+ const sessionId = init.headers["mcp-session-id"] as string;
840
+ await app.inject({
841
+ method: "POST",
842
+ url: "/mcp",
843
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
844
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
845
+ });
846
+
847
+ const call = await app.inject({
848
+ method: "POST",
849
+ url: "/mcp",
850
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
851
+ payload: {
852
+ jsonrpc: "2.0",
853
+ id: 3,
854
+ method: "tools/call",
855
+ params: {
856
+ name: "generate_image",
857
+ arguments: {
858
+ prompt: "edit",
859
+ image_path: inputPath,
860
+ },
861
+ },
862
+ },
863
+ });
864
+ assert.equal(call.statusCode, 200);
865
+ assert.ok(observedImageUrl?.startsWith("data:image/png;base64,"));
866
+ await app.close();
867
+ });
868
+
869
+ test("mcp generate_image rejects conflicting image_path and image_url", async () => {
870
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
871
+ const app = Fastify();
872
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
873
+ runImageGeneration: async () => {
874
+ throw new Error("should not run");
875
+ },
876
+ });
877
+
878
+ const init = await app.inject({
879
+ method: "POST",
880
+ url: "/mcp",
881
+ headers: MCP_HEADERS,
882
+ payload: {
883
+ jsonrpc: "2.0",
884
+ id: 1,
885
+ method: "initialize",
886
+ params: {
887
+ protocolVersion: "2025-06-18",
888
+ capabilities: {},
889
+ clientInfo: { name: "test", version: "1.0.0" },
890
+ },
891
+ },
892
+ });
893
+ const sessionId = init.headers["mcp-session-id"] as string;
894
+ await app.inject({
895
+ method: "POST",
896
+ url: "/mcp",
897
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
898
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
899
+ });
900
+
901
+ const call = await app.inject({
902
+ method: "POST",
903
+ url: "/mcp",
904
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
905
+ payload: {
906
+ jsonrpc: "2.0",
907
+ id: 3,
908
+ method: "tools/call",
909
+ params: {
910
+ name: "generate_image",
911
+ arguments: {
912
+ prompt: "edit",
913
+ image_path: "/tmp/a.png",
914
+ image_url: "https://example.com/a.png",
915
+ workspace_root: baseDir,
916
+ },
917
+ },
918
+ },
919
+ });
920
+ const callJson = call.json() as {
921
+ result?: { content?: Array<{ type: string; text?: string }>; isError?: boolean };
922
+ };
923
+ assert.equal(callJson.result?.isError, true);
924
+ assert.match(
925
+ callJson.result?.content?.find((item) => item.type === "text")?.text ?? "",
926
+ /"type":"invalid_request"/
927
+ );
928
+ await app.close();
929
+ });
930
+
931
+ test("mcp understand_image returns structured success output", async () => {
932
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
933
+ const app = Fastify();
934
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
935
+ runImageUnderstanding: async () => ({
936
+ model: "mock/vision",
937
+ analysis: {
938
+ answer: "A stop sign on a street",
939
+ ocr_text: "STOP",
940
+ objects: ["stop sign", "street"],
941
+ scene: "urban roadside",
942
+ notable_details: ["daylight"],
943
+ safety_notes: [],
944
+ },
945
+ raw_text: "A stop sign on a street",
946
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
947
+ }),
948
+ });
949
+
950
+ const init = await app.inject({
951
+ method: "POST",
952
+ url: "/mcp",
953
+ headers: MCP_HEADERS,
954
+ payload: {
955
+ jsonrpc: "2.0",
956
+ id: 1,
957
+ method: "initialize",
958
+ params: {
959
+ protocolVersion: "2025-06-18",
960
+ capabilities: {},
961
+ clientInfo: { name: "test", version: "1.0.0" },
962
+ },
963
+ },
964
+ });
965
+ const sessionId = init.headers["mcp-session-id"] as string;
966
+ await app.inject({
967
+ method: "POST",
968
+ url: "/mcp",
969
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
970
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
971
+ });
972
+
973
+ const call = await app.inject({
974
+ method: "POST",
975
+ url: "/mcp",
976
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
977
+ payload: {
978
+ jsonrpc: "2.0",
979
+ id: 3,
980
+ method: "tools/call",
981
+ params: {
982
+ name: "understand_image",
983
+ arguments: {
984
+ image_url: "data:image/png;base64,AQID",
985
+ },
986
+ },
987
+ },
988
+ });
989
+ assert.equal(call.statusCode, 200);
990
+ const callJson = call.json() as {
991
+ result?: {
992
+ content?: Array<{ type: string; text?: string }>;
993
+ isError?: boolean;
994
+ structuredContent?: {
995
+ ok: boolean;
996
+ text: string;
997
+ result: { ocr_text: string };
998
+ image_geometry?: unknown;
999
+ };
1000
+ };
1001
+ };
1002
+ assert.equal(callJson.result?.isError ?? false, false);
1003
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "{}";
1004
+ const payload = JSON.parse(text) as { ok: boolean; text: string; summary: string; model: string };
1005
+ assert.equal(payload.ok, true);
1006
+ assert.equal(payload.text, "A stop sign on a street");
1007
+ assert.equal(payload.summary, "Image analyzed.");
1008
+ assert.equal(callJson.result?.structuredContent?.result.ocr_text, "STOP");
1009
+ assert.equal(callJson.result?.structuredContent?.image_geometry, undefined);
1010
+
1011
+ await app.close();
1012
+ });
1013
+
1014
+ test("mcp understand_image forwards image_geometry when available", async () => {
1015
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
1016
+ const app = Fastify();
1017
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
1018
+ runImageUnderstanding: async () => ({
1019
+ model: "mock/vision",
1020
+ analysis: {
1021
+ answer: "Object at point",
1022
+ ocr_text: "",
1023
+ objects: ["object"],
1024
+ scene: "scene",
1025
+ notable_details: [],
1026
+ safety_notes: [],
1027
+ },
1028
+ raw_text: "Object at point",
1029
+ image_geometry: {
1030
+ original_width: 2000,
1031
+ original_height: 1200,
1032
+ uploaded_width: 1080,
1033
+ uploaded_height: 648,
1034
+ scale_x: 2000 / 1080,
1035
+ scale_y: 1200 / 648,
1036
+ resized: true,
1037
+ },
1038
+ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
1039
+ }),
1040
+ });
1041
+
1042
+ const init = await app.inject({
1043
+ method: "POST",
1044
+ url: "/mcp",
1045
+ headers: MCP_HEADERS,
1046
+ payload: {
1047
+ jsonrpc: "2.0",
1048
+ id: 1,
1049
+ method: "initialize",
1050
+ params: {
1051
+ protocolVersion: "2025-06-18",
1052
+ capabilities: {},
1053
+ clientInfo: { name: "test", version: "1.0.0" },
1054
+ },
1055
+ },
1056
+ });
1057
+ const sessionId = init.headers["mcp-session-id"] as string;
1058
+ await app.inject({
1059
+ method: "POST",
1060
+ url: "/mcp",
1061
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
1062
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
1063
+ });
1064
+
1065
+ const call = await app.inject({
1066
+ method: "POST",
1067
+ url: "/mcp",
1068
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
1069
+ payload: {
1070
+ jsonrpc: "2.0",
1071
+ id: 3,
1072
+ method: "tools/call",
1073
+ params: {
1074
+ name: "understand_image",
1075
+ arguments: {
1076
+ image_url: "data:image/png;base64,AQID",
1077
+ },
1078
+ },
1079
+ },
1080
+ });
1081
+ assert.equal(call.statusCode, 200);
1082
+ const callJson = call.json() as {
1083
+ result?: {
1084
+ content?: Array<{ type: string; text?: string }>;
1085
+ isError?: boolean;
1086
+ structuredContent?: {
1087
+ ok: boolean;
1088
+ image_geometry?: { original_width: number; resized: boolean };
1089
+ };
1090
+ };
1091
+ };
1092
+ assert.equal(callJson.result?.isError ?? false, false);
1093
+ const text = callJson.result?.content?.find((item) => item.type === "text")?.text ?? "{}";
1094
+ const payload = JSON.parse(text) as { ok: boolean; text: string };
1095
+ assert.equal(payload.ok, true);
1096
+ assert.equal(payload.text, "Object at point");
1097
+ assert.equal(callJson.result?.structuredContent?.image_geometry?.original_width, 2000);
1098
+ assert.equal(callJson.result?.structuredContent?.image_geometry?.resized, true);
1099
+
1100
+ await app.close();
1101
+ });
1102
+
1103
+ test("mcp understand_image rejects missing or conflicting image sources", async () => {
1104
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
1105
+ const app = Fastify();
1106
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
1107
+ runImageUnderstanding: async () => {
1108
+ throw new Error("should not run");
1109
+ },
1110
+ });
1111
+
1112
+ const init = await app.inject({
1113
+ method: "POST",
1114
+ url: "/mcp",
1115
+ headers: MCP_HEADERS,
1116
+ payload: {
1117
+ jsonrpc: "2.0",
1118
+ id: 1,
1119
+ method: "initialize",
1120
+ params: {
1121
+ protocolVersion: "2025-06-18",
1122
+ capabilities: {},
1123
+ clientInfo: { name: "test", version: "1.0.0" },
1124
+ },
1125
+ },
1126
+ });
1127
+ const sessionId = init.headers["mcp-session-id"] as string;
1128
+ await app.inject({
1129
+ method: "POST",
1130
+ url: "/mcp",
1131
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
1132
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
1133
+ });
1134
+
1135
+ const conflict = await app.inject({
1136
+ method: "POST",
1137
+ url: "/mcp",
1138
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
1139
+ payload: {
1140
+ jsonrpc: "2.0",
1141
+ id: 3,
1142
+ method: "tools/call",
1143
+ params: {
1144
+ name: "understand_image",
1145
+ arguments: {
1146
+ image_path: "/tmp/a.png",
1147
+ image_url: "https://example.com/a.png",
1148
+ },
1149
+ },
1150
+ },
1151
+ });
1152
+ const conflictJson = conflict.json() as {
1153
+ result?: { content?: Array<{ type: string; text?: string }>; isError?: boolean };
1154
+ };
1155
+ assert.equal(conflictJson.result?.isError, true);
1156
+ assert.match(
1157
+ conflictJson.result?.content?.find((item) => item.type === "text")?.text ?? "",
1158
+ /"type":"invalid_request"/
1159
+ );
1160
+
1161
+ const missing = await app.inject({
1162
+ method: "POST",
1163
+ url: "/mcp",
1164
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
1165
+ payload: {
1166
+ jsonrpc: "2.0",
1167
+ id: 4,
1168
+ method: "tools/call",
1169
+ params: {
1170
+ name: "understand_image",
1171
+ arguments: {},
1172
+ },
1173
+ },
1174
+ });
1175
+ const missingJson = missing.json() as {
1176
+ result?: { content?: Array<{ type: string; text?: string }>; isError?: boolean };
1177
+ };
1178
+ assert.equal(missingJson.result?.isError, true);
1179
+ assert.match(
1180
+ missingJson.result?.content?.find((item) => item.type === "text")?.text ?? "",
1181
+ /"type":"invalid_request"/
1182
+ );
1183
+
1184
+ await app.close();
1185
+ });
1186
+
1187
+ test("mcp understand_image returns typed no_vision_model error", async () => {
1188
+ const baseDir = await makeWorkspaceTempDir("waypoi-mcp-test-");
1189
+ const app = Fastify();
1190
+ await registerMcpServiceRoutes(app, makePaths(baseDir), {
1191
+ runImageUnderstanding: async () => {
1192
+ const err = new Error("No vision model");
1193
+ (err as Error & { type: string }).type = "no_vision_model";
1194
+ throw err;
1195
+ },
1196
+ });
1197
+
1198
+ const init = await app.inject({
1199
+ method: "POST",
1200
+ url: "/mcp",
1201
+ headers: MCP_HEADERS,
1202
+ payload: {
1203
+ jsonrpc: "2.0",
1204
+ id: 1,
1205
+ method: "initialize",
1206
+ params: {
1207
+ protocolVersion: "2025-06-18",
1208
+ capabilities: {},
1209
+ clientInfo: { name: "test", version: "1.0.0" },
1210
+ },
1211
+ },
1212
+ });
1213
+ const sessionId = init.headers["mcp-session-id"] as string;
1214
+ await app.inject({
1215
+ method: "POST",
1216
+ url: "/mcp",
1217
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
1218
+ payload: { jsonrpc: "2.0", method: "notifications/initialized", params: {} },
1219
+ });
1220
+
1221
+ const call = await app.inject({
1222
+ method: "POST",
1223
+ url: "/mcp",
1224
+ headers: { ...MCP_HEADERS, "mcp-session-id": sessionId },
1225
+ payload: {
1226
+ jsonrpc: "2.0",
1227
+ id: 3,
1228
+ method: "tools/call",
1229
+ params: {
1230
+ name: "understand_image",
1231
+ arguments: { image_url: "data:image/png;base64,AQID" },
1232
+ },
1233
+ },
1234
+ });
1235
+ const callJson = call.json() as {
1236
+ result?: { content?: Array<{ type: string; text?: string }>; isError?: boolean };
1237
+ };
1238
+ assert.equal(callJson.result?.isError, true);
1239
+ assert.match(
1240
+ callJson.result?.content?.find((item) => item.type === "text")?.text ?? "",
1241
+ /"type":"no_vision_model"/
1242
+ );
1243
+
1244
+ await app.close();
1245
+ });