Skip to content

Commit 6979d18

Browse files
committed
refactor!: trim mcp tools to core three
Drop summarize_video, extract_video_text, video_qa, compare_video_frames, and list_capabilities. The four video tools were prompt sugar over analyze_video; the model client can pass the same prompt directly. list_capabilities was static info the client does not need. BREAKING CHANGE: 5 tools removed. Clients referencing summarize_video, extract_video_text, video_qa, compare_video_frames, or list_capabilities will break. Migrate to analyze_video/analyze_image with an explicit prompt.
1 parent e45d96d commit 6979d18

5 files changed

Lines changed: 5 additions & 225 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ CI runs the same on Node 20 and 22. Local green ≠ CI green if you skip a step.
3939

4040
## Tool surface
4141

42-
The server exposes 8 MCP tools (see `src/server.ts`): `analyze_video`, `analyze_image`, `summarize_video`, `extract_video_text`, `video_qa`, `compare_video_frames`, `check_endpoint_status`, `list_capabilities`. Do not silently change a tool's name or argument schema — that breaks MCP clients. Add new tools rather than renaming.
42+
The server exposes 3 MCP tools (see `src/server.ts`): `analyze_video`, `analyze_image`, `check_endpoint_status`. Do not silently change a tool's name or argument schema — that breaks MCP clients. Add new tools rather than renaming.
4343

4444
`check_endpoint_status` must redact the API key (`redactKey`). There is a test asserting no key leaks — keep it passing.
4545

README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,7 @@ For local development without publishing:
8383
| ----------------------- | --------------------------------------------------------- |
8484
| `analyze_video` | Analyze a video (URL or local file) with a custom prompt |
8585
| `analyze_image` | Analyze an image (URL or local file) with a custom prompt |
86-
| `summarize_video` | Brief / standard / detailed summary |
87-
| `extract_video_text` | Extract on-screen text and transcribe speech |
88-
| `video_qa` | Ask a specific question about a video |
89-
| `compare_video_frames` | Analyze changes and progression across a video |
9086
| `check_endpoint_status` | Show configured endpoint/model (key redacted) |
91-
| `list_capabilities` | List server capabilities and supported formats |
9287

9388
Each media tool accepts a public `http`/`https` URL **or a local file path**. Local files are read and sent inline as base64 data URLs, with a 25MB guardrail (verified up to a 14MB video / ~18MB body, HTTP 200). Files larger than 25MB must be hosted at a public URL instead. Local input is validated by extension + magic-byte signature before encoding, so non-media files are rejected.
9489

src/prompts.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

src/server.ts

Lines changed: 0 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,6 @@ import {
1111
} from "./config.js";
1212
import { analyze, BailianError, type MediaKind } from "./bailian.js";
1313
import { isRemoteUrl, isLocalPath, resolveMedia } from "./media.js";
14-
import {
15-
SUMMARY_MAX_TOKENS,
16-
SUMMARY_PROMPTS,
17-
TEXT_EXTRACTION_PROMPT,
18-
qaPrompt,
19-
comparePrompt,
20-
DEFAULT_COMPARE_PROMPT,
21-
} from "./prompts.js";
2214

2315
const MAX_TOKENS_DEFAULT_VIDEO = 1024;
2416
const MAX_TOKENS_DEFAULT_IMAGE = 512;
@@ -119,70 +111,6 @@ export function createServer(cfg: AppConfig = loadConfig()): McpServer {
119111
async (args) => mediaCall(cfg, "image", args.image_url, args.question, args.max_tokens),
120112
);
121113

122-
server.registerTool(
123-
"summarize_video",
124-
{
125-
description:
126-
"Generate a summary of a video. Styles: brief (1-2 sentences), standard (1-2 paragraphs), detailed (comprehensive timeline).",
127-
inputSchema: {
128-
video_url: mediaInput("Public URL or local file path of the video to summarize"),
129-
style: z
130-
.enum(["brief", "standard", "detailed"])
131-
.default("standard")
132-
.describe("Summary style"),
133-
},
134-
},
135-
async (args) =>
136-
mediaCall(
137-
cfg,
138-
"video",
139-
args.video_url,
140-
SUMMARY_PROMPTS[args.style],
141-
SUMMARY_MAX_TOKENS[args.style],
142-
),
143-
);
144-
145-
server.registerTool(
146-
"extract_video_text",
147-
{
148-
description:
149-
"Extract and transcribe visible text or speech from a video (on-screen text, captions, speech, slide text).",
150-
inputSchema: {
151-
video_url: mediaInput("Public URL or local file path of the video"),
152-
},
153-
},
154-
async (args) => mediaCall(cfg, "video", args.video_url, TEXT_EXTRACTION_PROMPT, 1024),
155-
);
156-
157-
server.registerTool(
158-
"video_qa",
159-
{
160-
description: "Ask a specific question about a video's content.",
161-
inputSchema: {
162-
video_url: mediaInput("Public URL or local file path of the video"),
163-
question: z.string().describe("Your specific question about the video"),
164-
},
165-
},
166-
async (args) => mediaCall(cfg, "video", args.video_url, qaPrompt(args.question), 512),
167-
);
168-
169-
server.registerTool(
170-
"compare_video_frames",
171-
{
172-
description:
173-
"Analyze changes and progression across a video (before/after, movement, progression of events).",
174-
inputSchema: {
175-
video_url: mediaInput("Public URL or local file path of the video"),
176-
comparison_prompt: z
177-
.string()
178-
.default(DEFAULT_COMPARE_PROMPT)
179-
.describe("What to compare across the video"),
180-
},
181-
},
182-
async (args) =>
183-
mediaCall(cfg, "video", args.video_url, comparePrompt(args.comparison_prompt), 1024),
184-
);
185-
186114
server.registerTool(
187115
"check_endpoint_status",
188116
{
@@ -205,41 +133,6 @@ export function createServer(cfg: AppConfig = loadConfig()): McpServer {
205133
),
206134
);
207135

208-
server.registerTool(
209-
"list_capabilities",
210-
{
211-
description: "List the capabilities of this MCP server.",
212-
},
213-
() =>
214-
ok(
215-
JSON.stringify(
216-
{
217-
model: cfg.model,
218-
backend: "Bailian (DashScope) OpenAI-compatible endpoint",
219-
capabilities: [
220-
"Video understanding (native, no frame extraction)",
221-
"Image understanding",
222-
"Video summarization",
223-
"Video Q&A",
224-
"Text extraction from video",
225-
"Scene change / progression analysis",
226-
],
227-
supported_formats: {
228-
video: ["mp4", "webm", "mov", "avi", "mkv"],
229-
image: ["jpg", "jpeg", "png", "gif", "webp", "bmp"],
230-
},
231-
notes: [
232-
"Media: public http/https URL or local file path (local files are sent inline as base64 data URLs)",
233-
"Local file size guardrail: 25MB; larger files must be hosted at a public URL",
234-
"Video frame sampling is handled by Bailian server-side (fixed 0.5s/frame on OpenAI-compatible mode)",
235-
],
236-
},
237-
null,
238-
2,
239-
),
240-
),
241-
);
242-
243136
return server;
244137
}
245138

test/tools.test.ts

Lines changed: 4 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
88
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
99
import { type McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1010
import { type AppConfig } from "../src/config.js";
11-
import { DEFAULT_COMPARE_PROMPT } from "../src/prompts.js";
1211
import { createServer } from "../src/server.js";
1312

1413
const SECRET_KEY = "sk-secret-key-1234567890"; // gitleaks:allow — dummy test fixture, not a real key
@@ -76,20 +75,11 @@ function textOf(result: unknown): string {
7675
}
7776

7877
describe("MCP tool wiring (in-memory e2e)", () => {
79-
it("exposes all 8 tools", async () => {
78+
it("exposes all 3 tools", async () => {
8079
await withClient(async (client) => {
8180
const { tools } = await client.listTools();
8281
expect(tools.map((t) => t.name).sort()).toEqual(
83-
[
84-
"analyze_image",
85-
"analyze_video",
86-
"check_endpoint_status",
87-
"compare_video_frames",
88-
"extract_video_text",
89-
"list_capabilities",
90-
"summarize_video",
91-
"video_qa",
92-
].sort(),
82+
["analyze_image", "analyze_video", "check_endpoint_status"].sort(),
9383
);
9484
});
9585
});
@@ -121,21 +111,6 @@ describe("MCP tool wiring (in-memory e2e)", () => {
121111
expect(body.max_tokens).toBe(512);
122112
});
123113

124-
it("summarize_video detailed uses the detailed prompt and 1024 tokens", async () => {
125-
const cap = mockCapture();
126-
await withClient(async (client) => {
127-
const r = await client.callTool({
128-
name: "summarize_video",
129-
arguments: { video_url: "https://v/x.mp4", style: "detailed" },
130-
});
131-
expect(textOf(r)).toBe("answer");
132-
});
133-
const body = await cap.body();
134-
expect(body.max_tokens).toBe(1024);
135-
const prompt = (body.messages as { content: { text?: string }[] }[])[0]!.content[0]!.text ?? "";
136-
expect(prompt).toContain("comprehensive");
137-
});
138-
139114
it("maps a backend 500 to an isError tool result", async () => {
140115
server.use(http.post(endpoint, () => new HttpResponse(null, { status: 500 })));
141116
await withClient(async (client) => {
@@ -148,36 +123,6 @@ describe("MCP tool wiring (in-memory e2e)", () => {
148123
});
149124
});
150125

151-
it("video_qa wraps the question and uses 512 tokens", async () => {
152-
const cap = mockCapture();
153-
await withClient(async (client) => {
154-
const r = await client.callTool({
155-
name: "video_qa",
156-
arguments: { video_url: "https://v/x.mp4", question: "how many cats?" },
157-
});
158-
expect(textOf(r)).toBe("answer");
159-
});
160-
const body = await cap.body();
161-
const prompt = (body.messages as { content: { text?: string }[] }[])[0]!.content[0]!.text ?? "";
162-
expect(prompt).toContain("how many cats?");
163-
expect(body.max_tokens).toBe(512);
164-
});
165-
166-
it("compare_video_frames applies the default comparison prompt", async () => {
167-
const cap = mockCapture();
168-
await withClient(async (client) => {
169-
const r = await client.callTool({
170-
name: "compare_video_frames",
171-
arguments: { video_url: "https://v/x.mp4" },
172-
});
173-
expect(textOf(r)).toBe("answer");
174-
});
175-
const body = await cap.body();
176-
const prompt = (body.messages as { content: { text?: string }[] }[])[0]!.content[0]!.text ?? "";
177-
expect(prompt).toContain(DEFAULT_COMPARE_PROMPT);
178-
expect(body.max_tokens).toBe(1024);
179-
});
180-
181126
it("check_endpoint_status never leaks the API key", async () => {
182127
await withClient(async (client) => {
183128
const r = await client.callTool({ name: "check_endpoint_status", arguments: {} });
@@ -187,15 +132,6 @@ describe("MCP tool wiring (in-memory e2e)", () => {
187132
expect(text).toContain("qwen3.7-plus");
188133
});
189134
});
190-
191-
it("list_capabilities reports the configured model", async () => {
192-
await withClient(async (client) => {
193-
const r = await client.callTool({ name: "list_capabilities", arguments: {} });
194-
const parsed = JSON.parse(textOf(r)) as { model: string; capabilities: string[] };
195-
expect(parsed.model).toBe("qwen3.7-plus");
196-
expect(parsed.capabilities).toContain("Video understanding (native, no frame extraction)");
197-
});
198-
});
199135
});
200136

201137
function mediaUrlOf(body: Record<string, unknown>): string {
@@ -257,8 +193,8 @@ describe("local file path support", () => {
257193
const cap = mockCapture();
258194
await withClient(async (client) => {
259195
await client.callTool({
260-
name: "summarize_video",
261-
arguments: { video_url: "https://example.com/v.mp4", style: "brief" },
196+
name: "analyze_video",
197+
arguments: { video_url: "https://example.com/v.mp4", question: "summarize" },
262198
});
263199
});
264200
expect(mediaUrlOf(await cap.body())).toBe("https://example.com/v.mp4");

0 commit comments

Comments
 (0)