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,611 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { IncomingMessage, ServerResponse } from "http";
|
|
3
|
+
import { promises as fs } from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
8
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { StoragePaths } from "../storage/files";
|
|
10
|
+
import {
|
|
11
|
+
normalizeImageGenerationPayload,
|
|
12
|
+
runImageGeneration,
|
|
13
|
+
} from "../services/imageGeneration";
|
|
14
|
+
import { imageDataUrlFromPath, runImageUnderstanding } from "../services/imageUnderstanding";
|
|
15
|
+
import { resolveVideoGenerationModel, runVideoGeneration } from "../services/videoGeneration";
|
|
16
|
+
import { ImageGenerationRequest, VideoGenerationRequest } from "../types";
|
|
17
|
+
import {
|
|
18
|
+
validateAtMostOneImageInput,
|
|
19
|
+
resolveBinaryOutputPolicy,
|
|
20
|
+
typedError,
|
|
21
|
+
validateSingleImageInput,
|
|
22
|
+
} from "./policy";
|
|
23
|
+
|
|
24
|
+
interface McpSessionEntry {
|
|
25
|
+
server: McpServer;
|
|
26
|
+
transport: StreamableHTTPServerTransport;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface McpServiceDependencies {
|
|
30
|
+
runImageGeneration: typeof runImageGeneration;
|
|
31
|
+
normalizeImageGenerationPayload: typeof normalizeImageGenerationPayload;
|
|
32
|
+
runImageUnderstanding: typeof runImageUnderstanding;
|
|
33
|
+
runVideoGeneration: typeof runVideoGeneration;
|
|
34
|
+
resolveVideoGenerationModel: typeof resolveVideoGenerationModel;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type McpServiceDependencyOverrides = Partial<McpServiceDependencies>;
|
|
38
|
+
|
|
39
|
+
export interface McpService {
|
|
40
|
+
handleRequest(req: IncomingMessage, res: ServerResponse, parsedBody?: unknown): Promise<void>;
|
|
41
|
+
close(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const defaultDeps: McpServiceDependencies = {
|
|
45
|
+
runImageGeneration,
|
|
46
|
+
normalizeImageGenerationPayload,
|
|
47
|
+
runImageUnderstanding,
|
|
48
|
+
runVideoGeneration,
|
|
49
|
+
resolveVideoGenerationModel,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const GENERATE_IMAGE_TOOL_DESCRIPTION =
|
|
53
|
+
"Generate or edit images from a text prompt. Supports text-to-image and image-to-image editing. Successful calls always write files to the waypoi config directory (~/.config/waypoi/generated-images by default; override with WAYPOI_MCP_OUTPUT_ROOT). Report file_path or file_paths to the user. Use include_data=true only when inline image data is also needed.";
|
|
54
|
+
|
|
55
|
+
const UNDERSTAND_IMAGE_TOOL_DESCRIPTION =
|
|
56
|
+
"Analyze one image and return structured text. Provide exactly one of image_path (local file) or image_url (http/https or data URL). Use the instruction parameter to specify what analysis you need (e.g., 'describe the image', 'find all objects and their bounding boxes', 'extract text via OCR'). If returning points or boxes, use original-image pixel coordinates. Keep instruction concise and specify the output format you expect.";
|
|
57
|
+
|
|
58
|
+
const GENERATE_VIDEO_TOOL_DESCRIPTION =
|
|
59
|
+
"Generate videos from text prompts or images. Supports text-to-video and image-to-video generation using Alibaba Cloud ModelStudio (Wan models). Videos are generated asynchronously and may take 1-5 minutes. Returns a URL to the generated MP4 video (H.264 encoding). Provide a detailed prompt describing the desired video content, style, and camera movement. Optionally provide an image_url to use as the first frame for image-to-video generation.";
|
|
60
|
+
|
|
61
|
+
export function createMcpService(
|
|
62
|
+
paths: StoragePaths,
|
|
63
|
+
deps: McpServiceDependencyOverrides = {}
|
|
64
|
+
): McpService {
|
|
65
|
+
const resolvedDeps: McpServiceDependencies = {
|
|
66
|
+
...defaultDeps,
|
|
67
|
+
...deps,
|
|
68
|
+
};
|
|
69
|
+
const sessions = new Map<string, McpSessionEntry>();
|
|
70
|
+
|
|
71
|
+
const createServer = (): McpServer => {
|
|
72
|
+
const server = new McpServer(
|
|
73
|
+
{
|
|
74
|
+
name: "waypoi-mcp",
|
|
75
|
+
version: "0.7.1",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
capabilities: {},
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
server.registerTool(
|
|
83
|
+
"generate_image",
|
|
84
|
+
{
|
|
85
|
+
description: GENERATE_IMAGE_TOOL_DESCRIPTION,
|
|
86
|
+
inputSchema: {
|
|
87
|
+
prompt: z.string().min(1),
|
|
88
|
+
model: z.string().optional(),
|
|
89
|
+
image_path: z.string().optional(),
|
|
90
|
+
image_url: z.string().optional(),
|
|
91
|
+
n: z.number().int().min(1).max(4).optional(),
|
|
92
|
+
size: z.string().optional(),
|
|
93
|
+
quality: z.string().optional(),
|
|
94
|
+
style: z.string().optional(),
|
|
95
|
+
response_format: z.enum(["url", "b64_json"]).optional(),
|
|
96
|
+
include_data: z.boolean().optional(),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
async (args) => {
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
const timeout = setTimeout(() => controller.abort(), 60_000);
|
|
102
|
+
try {
|
|
103
|
+
validateAtMostOneImageInput({
|
|
104
|
+
image_path: args.image_path,
|
|
105
|
+
image_url: args.image_url,
|
|
106
|
+
});
|
|
107
|
+
const resolvedImageUrl = await resolveOptionalImageInputToUrl({
|
|
108
|
+
image_path: args.image_path,
|
|
109
|
+
image_url: args.image_url,
|
|
110
|
+
});
|
|
111
|
+
const filePolicy = resolveBinaryOutputPolicy(
|
|
112
|
+
{ include_data: args.include_data },
|
|
113
|
+
{ defaultBaseDir: paths.baseDir }
|
|
114
|
+
);
|
|
115
|
+
const hasFileOutput = true;
|
|
116
|
+
// File output requires decodable bytes; force b64_json upstream even if caller asks for "url".
|
|
117
|
+
const responseFormat = hasFileOutput ? "b64_json" : (args.response_format ?? "b64_json");
|
|
118
|
+
const request: ImageGenerationRequest = {
|
|
119
|
+
prompt: args.prompt,
|
|
120
|
+
model: args.model,
|
|
121
|
+
n: args.n,
|
|
122
|
+
size: args.size,
|
|
123
|
+
quality: args.quality,
|
|
124
|
+
style: args.style,
|
|
125
|
+
response_format: responseFormat,
|
|
126
|
+
image_url: resolvedImageUrl,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const generated = await resolvedDeps.runImageGeneration(paths, request, {}, controller.signal);
|
|
130
|
+
const normalized = await resolvedDeps.normalizeImageGenerationPayload(
|
|
131
|
+
paths,
|
|
132
|
+
generated.payload,
|
|
133
|
+
generated.model
|
|
134
|
+
);
|
|
135
|
+
const artifacts = await materializeImagesToFiles(normalized.images, normalized.created, {
|
|
136
|
+
outputDir: filePolicy.outputDir,
|
|
137
|
+
includeData: filePolicy.includeData,
|
|
138
|
+
outputBaseRoot: filePolicy.outputBaseRoot,
|
|
139
|
+
});
|
|
140
|
+
const filePaths = artifacts.map((artifact) => artifact.file_path);
|
|
141
|
+
const summary = buildGenerateImageSummary(filePaths.length);
|
|
142
|
+
|
|
143
|
+
const output = {
|
|
144
|
+
ok: true,
|
|
145
|
+
summary,
|
|
146
|
+
model: normalized.model,
|
|
147
|
+
created: normalized.created,
|
|
148
|
+
...(filePaths.length === 1
|
|
149
|
+
? { file_path: filePaths[0] }
|
|
150
|
+
: { file_paths: filePaths }),
|
|
151
|
+
artifacts,
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
content: [
|
|
155
|
+
{
|
|
156
|
+
type: "text" as const,
|
|
157
|
+
text: JSON.stringify(buildGenerateImageTextPayload(output)),
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
structuredContent: output,
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const typed = error as Error & { type?: string };
|
|
164
|
+
const type = typed.type ?? "upstream_error";
|
|
165
|
+
const message =
|
|
166
|
+
type === "no_diffusion_model"
|
|
167
|
+
? "No diffusion model available. Add or enable a provider model."
|
|
168
|
+
: typed.message || "Image generation failed";
|
|
169
|
+
const output = {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: {
|
|
172
|
+
type,
|
|
173
|
+
message,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
isError: true,
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: "text" as const,
|
|
181
|
+
text: JSON.stringify(output),
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
structuredContent: output,
|
|
185
|
+
};
|
|
186
|
+
} finally {
|
|
187
|
+
clearTimeout(timeout);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
server.registerTool(
|
|
193
|
+
"understand_image",
|
|
194
|
+
{
|
|
195
|
+
description: UNDERSTAND_IMAGE_TOOL_DESCRIPTION,
|
|
196
|
+
inputSchema: {
|
|
197
|
+
image_path: z.string().optional(),
|
|
198
|
+
image_url: z.string().optional(),
|
|
199
|
+
instruction: z.string().optional(),
|
|
200
|
+
model: z.string().optional(),
|
|
201
|
+
max_tokens: z.number().int().min(1).max(4096).optional(),
|
|
202
|
+
temperature: z.number().min(0).max(2).optional(),
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
async (args) => {
|
|
206
|
+
const controller = new AbortController();
|
|
207
|
+
const timeout = setTimeout(() => controller.abort(), 60_000);
|
|
208
|
+
try {
|
|
209
|
+
validateSingleImageInput({
|
|
210
|
+
image_path: args.image_path,
|
|
211
|
+
image_url: args.image_url,
|
|
212
|
+
});
|
|
213
|
+
const result = await resolvedDeps.runImageUnderstanding(
|
|
214
|
+
paths,
|
|
215
|
+
{
|
|
216
|
+
image_path: args.image_path,
|
|
217
|
+
image_url: args.image_url,
|
|
218
|
+
instruction: args.instruction,
|
|
219
|
+
model: args.model,
|
|
220
|
+
max_tokens: args.max_tokens,
|
|
221
|
+
temperature: args.temperature,
|
|
222
|
+
},
|
|
223
|
+
controller.signal
|
|
224
|
+
);
|
|
225
|
+
const summary = "Image analyzed.";
|
|
226
|
+
const output = {
|
|
227
|
+
ok: true,
|
|
228
|
+
summary,
|
|
229
|
+
model: result.model,
|
|
230
|
+
text: result.raw_text,
|
|
231
|
+
result: result.analysis,
|
|
232
|
+
...(result.image_geometry ? { image_geometry: result.image_geometry } : {}),
|
|
233
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
content: [
|
|
237
|
+
{
|
|
238
|
+
type: "text" as const,
|
|
239
|
+
text: JSON.stringify({
|
|
240
|
+
ok: true,
|
|
241
|
+
summary,
|
|
242
|
+
model: result.model,
|
|
243
|
+
text: result.raw_text,
|
|
244
|
+
}),
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
structuredContent: output,
|
|
248
|
+
};
|
|
249
|
+
} catch (error) {
|
|
250
|
+
const typed = error as Error & { type?: string };
|
|
251
|
+
const type = typed.type ?? "upstream_error";
|
|
252
|
+
const output = {
|
|
253
|
+
ok: false,
|
|
254
|
+
error: {
|
|
255
|
+
type,
|
|
256
|
+
message: typed.message || "Image understanding failed",
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
return {
|
|
260
|
+
isError: true,
|
|
261
|
+
content: [{ type: "text" as const, text: JSON.stringify(output) }],
|
|
262
|
+
structuredContent: output,
|
|
263
|
+
};
|
|
264
|
+
} finally {
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
server.registerTool(
|
|
271
|
+
"generate_video",
|
|
272
|
+
{
|
|
273
|
+
description: GENERATE_VIDEO_TOOL_DESCRIPTION,
|
|
274
|
+
inputSchema: {
|
|
275
|
+
prompt: z.string().min(1),
|
|
276
|
+
model: z.string().optional(),
|
|
277
|
+
image_url: z.string().optional(),
|
|
278
|
+
audio_url: z.string().optional(),
|
|
279
|
+
duration: z.number().int().min(2).max(15).optional(),
|
|
280
|
+
resolution: z.enum(["480P", "720P", "1080P"]).optional(),
|
|
281
|
+
negative_prompt: z.string().optional(),
|
|
282
|
+
seed: z.number().int().min(0).max(2147483647).optional(),
|
|
283
|
+
watermark: z.boolean().optional(),
|
|
284
|
+
prompt_extend: z.boolean().optional(),
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
async (args) => {
|
|
288
|
+
const controller = new AbortController();
|
|
289
|
+
const timeout = setTimeout(() => controller.abort(), 300_000);
|
|
290
|
+
try {
|
|
291
|
+
const resolvedModel = await resolvedDeps.resolveVideoGenerationModel(paths, args.model);
|
|
292
|
+
if (!resolvedModel) {
|
|
293
|
+
throw typedError("no_video_model", "No video generation model available. Add or enable a provider model.");
|
|
294
|
+
}
|
|
295
|
+
const request: VideoGenerationRequest = {
|
|
296
|
+
prompt: args.prompt,
|
|
297
|
+
model: resolvedModel,
|
|
298
|
+
image_url: args.image_url,
|
|
299
|
+
audio_url: args.audio_url,
|
|
300
|
+
duration: args.duration,
|
|
301
|
+
resolution: args.resolution,
|
|
302
|
+
negative_prompt: args.negative_prompt,
|
|
303
|
+
seed: args.seed,
|
|
304
|
+
watermark: args.watermark,
|
|
305
|
+
prompt_extend: args.prompt_extend,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const generated = await resolvedDeps.runVideoGeneration(paths, request, {}, controller.signal);
|
|
309
|
+
const payload = generated.payload as { data?: Array<{ url?: string; revised_prompt?: string }>; usage?: { video_count?: number; duration?: number; resolution?: string } };
|
|
310
|
+
const data = payload.data ?? [];
|
|
311
|
+
const usage = payload.usage ?? {};
|
|
312
|
+
|
|
313
|
+
if (data.length === 0) {
|
|
314
|
+
throw typedError("no_video_output", "Video generation completed but no video URL was returned.");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const videoData = data[0];
|
|
318
|
+
const output = {
|
|
319
|
+
ok: true,
|
|
320
|
+
summary: "Generated 1 video.",
|
|
321
|
+
model: generated.route.upstreamModel,
|
|
322
|
+
url: videoData.url,
|
|
323
|
+
...(videoData.revised_prompt ? { revised_prompt: videoData.revised_prompt } : {}),
|
|
324
|
+
...(usage.video_count ? { video_count: usage.video_count } : {}),
|
|
325
|
+
...(usage.duration ? { duration: usage.duration } : {}),
|
|
326
|
+
...(usage.resolution ? { resolution: usage.resolution } : {}),
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
content: [
|
|
330
|
+
{
|
|
331
|
+
type: "text" as const,
|
|
332
|
+
text: JSON.stringify(output),
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
structuredContent: output,
|
|
336
|
+
};
|
|
337
|
+
} catch (error) {
|
|
338
|
+
const typed = error as Error & { type?: string };
|
|
339
|
+
const type = typed.type ?? "upstream_error";
|
|
340
|
+
const message =
|
|
341
|
+
type === "no_video_model"
|
|
342
|
+
? "No video generation model available. Add or enable a provider model."
|
|
343
|
+
: typed.message || "Video generation failed";
|
|
344
|
+
const output = {
|
|
345
|
+
ok: false,
|
|
346
|
+
error: {
|
|
347
|
+
type,
|
|
348
|
+
message,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
isError: true,
|
|
353
|
+
content: [
|
|
354
|
+
{
|
|
355
|
+
type: "text" as const,
|
|
356
|
+
text: JSON.stringify(output),
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
structuredContent: output,
|
|
360
|
+
};
|
|
361
|
+
} finally {
|
|
362
|
+
clearTimeout(timeout);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
return server;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const close = async (): Promise<void> => {
|
|
371
|
+
const entries = Array.from(sessions.values());
|
|
372
|
+
sessions.clear();
|
|
373
|
+
await Promise.allSettled(
|
|
374
|
+
entries.map(async (entry) => {
|
|
375
|
+
await entry.transport.close();
|
|
376
|
+
await entry.server.close();
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const handleRequest = async (
|
|
382
|
+
req: IncomingMessage,
|
|
383
|
+
res: ServerResponse,
|
|
384
|
+
parsedBody?: unknown
|
|
385
|
+
): Promise<void> => {
|
|
386
|
+
const sessionIdHeader = req.headers["mcp-session-id"];
|
|
387
|
+
const sessionId =
|
|
388
|
+
typeof sessionIdHeader === "string"
|
|
389
|
+
? sessionIdHeader
|
|
390
|
+
: Array.isArray(sessionIdHeader)
|
|
391
|
+
? sessionIdHeader[0]
|
|
392
|
+
: undefined;
|
|
393
|
+
|
|
394
|
+
let entry: McpSessionEntry | undefined;
|
|
395
|
+
if (sessionId) {
|
|
396
|
+
entry = sessions.get(sessionId);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!entry) {
|
|
400
|
+
if (sessionId || !isInitializeRequest(parsedBody)) {
|
|
401
|
+
if (!res.headersSent) {
|
|
402
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
403
|
+
res.end(
|
|
404
|
+
JSON.stringify({
|
|
405
|
+
jsonrpc: "2.0",
|
|
406
|
+
error: {
|
|
407
|
+
code: -32000,
|
|
408
|
+
message: "Bad Request: No valid MCP session ID provided",
|
|
409
|
+
},
|
|
410
|
+
id: null,
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const server = createServer();
|
|
418
|
+
const transport = new StreamableHTTPServerTransport({
|
|
419
|
+
sessionIdGenerator: () => randomUUID(),
|
|
420
|
+
enableJsonResponse: true,
|
|
421
|
+
onsessioninitialized: (newSessionId) => {
|
|
422
|
+
sessions.set(newSessionId, { server, transport });
|
|
423
|
+
},
|
|
424
|
+
onsessionclosed: (closedSessionId) => {
|
|
425
|
+
sessions.delete(closedSessionId);
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
transport.onclose = () => {
|
|
429
|
+
const sid = transport.sessionId;
|
|
430
|
+
if (sid) {
|
|
431
|
+
sessions.delete(sid);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
await server.connect(transport);
|
|
435
|
+
entry = { server, transport };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
await entry.transport.handleRequest(req, res, parsedBody);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
handleRequest,
|
|
443
|
+
close,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
type FileMaterializeOptions = {
|
|
448
|
+
outputDir: string;
|
|
449
|
+
includeData: boolean;
|
|
450
|
+
outputBaseRoot: string;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
async function materializeImagesToFiles(
|
|
454
|
+
images: Array<{ index: number; url?: string; b64_json?: string; revised_prompt?: string }>,
|
|
455
|
+
created: number,
|
|
456
|
+
options: FileMaterializeOptions
|
|
457
|
+
): Promise<
|
|
458
|
+
Array<{
|
|
459
|
+
index: number;
|
|
460
|
+
file_path: string;
|
|
461
|
+
mime_type: string;
|
|
462
|
+
bytes: number;
|
|
463
|
+
revised_prompt?: string;
|
|
464
|
+
url?: string;
|
|
465
|
+
b64_json?: string;
|
|
466
|
+
}>
|
|
467
|
+
> {
|
|
468
|
+
const output: Array<{
|
|
469
|
+
index: number;
|
|
470
|
+
file_path: string;
|
|
471
|
+
mime_type: string;
|
|
472
|
+
bytes: number;
|
|
473
|
+
revised_prompt?: string;
|
|
474
|
+
url?: string;
|
|
475
|
+
b64_json?: string;
|
|
476
|
+
}> = [];
|
|
477
|
+
|
|
478
|
+
for (const image of images) {
|
|
479
|
+
const payload = decodeImagePayload(image);
|
|
480
|
+
if (!payload) {
|
|
481
|
+
throw typedError(
|
|
482
|
+
"invalid_request",
|
|
483
|
+
`Image ${image.index} has no decodable bytes for file output.`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const extension = extensionForMime(payload.mimeType);
|
|
487
|
+
const resolvedPath = resolveOutputPath(image.index, created, extension, options);
|
|
488
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
489
|
+
await fs.writeFile(resolvedPath, payload.buffer);
|
|
490
|
+
|
|
491
|
+
const entry: {
|
|
492
|
+
index: number;
|
|
493
|
+
file_path: string;
|
|
494
|
+
mime_type: string;
|
|
495
|
+
bytes: number;
|
|
496
|
+
revised_prompt?: string;
|
|
497
|
+
url?: string;
|
|
498
|
+
b64_json?: string;
|
|
499
|
+
} = {
|
|
500
|
+
index: image.index,
|
|
501
|
+
file_path: toRelativeFilePath(options.outputBaseRoot, resolvedPath),
|
|
502
|
+
mime_type: payload.mimeType,
|
|
503
|
+
bytes: payload.buffer.length,
|
|
504
|
+
};
|
|
505
|
+
if (image.revised_prompt) {
|
|
506
|
+
entry.revised_prompt = image.revised_prompt;
|
|
507
|
+
}
|
|
508
|
+
if (options.includeData) {
|
|
509
|
+
if (image.url) entry.url = image.url;
|
|
510
|
+
if (image.b64_json) entry.b64_json = image.b64_json;
|
|
511
|
+
}
|
|
512
|
+
output.push(entry);
|
|
513
|
+
}
|
|
514
|
+
return output;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function decodeImagePayload(image: { url?: string; b64_json?: string }): { buffer: Buffer; mimeType: string } | null {
|
|
518
|
+
if (image.b64_json) {
|
|
519
|
+
const mimeType = extractMimeFromDataUrl(image.url) ?? "image/png";
|
|
520
|
+
return { buffer: Buffer.from(image.b64_json, "base64"), mimeType };
|
|
521
|
+
}
|
|
522
|
+
if (image.url?.startsWith("data:")) {
|
|
523
|
+
const match = image.url.match(/^data:([^;]+);base64,(.+)$/i);
|
|
524
|
+
if (!match) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
return { buffer: Buffer.from(match[2], "base64"), mimeType: match[1] };
|
|
528
|
+
}
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function extractMimeFromDataUrl(url?: string): string | null {
|
|
533
|
+
if (!url?.startsWith("data:")) {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
const match = url.match(/^data:([^;]+);base64,/i);
|
|
537
|
+
return match ? match[1] : null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function extensionForMime(mimeType: string): string {
|
|
541
|
+
switch (mimeType.toLowerCase()) {
|
|
542
|
+
case "image/jpeg":
|
|
543
|
+
return "jpg";
|
|
544
|
+
case "image/webp":
|
|
545
|
+
return "webp";
|
|
546
|
+
case "image/gif":
|
|
547
|
+
return "gif";
|
|
548
|
+
default:
|
|
549
|
+
return "png";
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function resolveOutputPath(
|
|
554
|
+
index: number,
|
|
555
|
+
created: number,
|
|
556
|
+
extension: string,
|
|
557
|
+
options: FileMaterializeOptions
|
|
558
|
+
): string {
|
|
559
|
+
return path.join(options.outputDir, `image-${created}-${index}.${extension}`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function toRelativeFilePath(outputBaseRoot: string, resolvedPath: string): string {
|
|
563
|
+
const relative = path.relative(outputBaseRoot, resolvedPath);
|
|
564
|
+
if (!relative || relative === "") {
|
|
565
|
+
return ".";
|
|
566
|
+
}
|
|
567
|
+
return relative.split(path.sep).join("/");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function resolveOptionalImageInputToUrl(input: {
|
|
571
|
+
image_path?: string;
|
|
572
|
+
image_url?: string;
|
|
573
|
+
}): Promise<string | undefined> {
|
|
574
|
+
if (input.image_path) {
|
|
575
|
+
return imageDataUrlFromPath(input.image_path);
|
|
576
|
+
}
|
|
577
|
+
if (!input.image_url) {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
if (
|
|
581
|
+
input.image_url.startsWith("data:image/") ||
|
|
582
|
+
input.image_url.startsWith("http://") ||
|
|
583
|
+
input.image_url.startsWith("https://")
|
|
584
|
+
) {
|
|
585
|
+
return input.image_url;
|
|
586
|
+
}
|
|
587
|
+
throw typedError(
|
|
588
|
+
"invalid_request",
|
|
589
|
+
"image_url must be an http(s) URL or data:image/* URL."
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function buildGenerateImageSummary(count: number): string {
|
|
594
|
+
return count === 1 ? "Generated 1 image file." : `Generated ${count} image files.`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function buildGenerateImageTextPayload(output: {
|
|
598
|
+
ok: boolean;
|
|
599
|
+
summary: string;
|
|
600
|
+
model: string;
|
|
601
|
+
file_path?: string;
|
|
602
|
+
file_paths?: string[];
|
|
603
|
+
}): Record<string, unknown> {
|
|
604
|
+
return {
|
|
605
|
+
ok: output.ok,
|
|
606
|
+
summary: output.summary,
|
|
607
|
+
...(output.file_path ? { file_path: output.file_path } : {}),
|
|
608
|
+
...(output.file_paths ? { file_paths: output.file_paths } : {}),
|
|
609
|
+
model: output.model,
|
|
610
|
+
};
|
|
611
|
+
}
|