Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions examples/pdf-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import {
getAnnotationPreview,
getAnnotationColor,
} from "./annotation-panel.js";
import { resolveToolName } from "./tool-names.js";
import "./global.css";
import "./mcp-app.css";

Expand Down Expand Up @@ -706,6 +707,16 @@ const app = new App(
{ autoResize: false },
);

/**
* Name to use for `app.callServerTool` when calling one of this server's own
* tools. Hosts that aggregate servers may qualify tool names (for example
* `pdf-server-save_pdf`); the prefix is recovered from the name the host
* invoked `display_pdf` under. See tool-names.ts and ext-apps issue #753.
*/
function toolName(baseName: string): string {
return resolveToolName(baseName, app.getHostContext()?.toolInfo?.tool?.name);
}

// UI State functions
function showLoading(text: string) {
loadingTextEl.textContent = text;
Expand Down Expand Up @@ -2575,7 +2586,7 @@ async function handleGetViewerState(requestId: string): Promise<void> {
};

await app.callServerTool({
name: "submit_viewer_state",
name: toolName("submit_viewer_state"),
arguments: { requestId, state: JSON.stringify(state, null, 2) },
});
}
Expand Down Expand Up @@ -2662,7 +2673,7 @@ async function handleGetPages(cmd: {
// Submit results back to server
try {
await app.callServerTool({
name: "submit_page_data",
name: toolName("submit_page_data"),
arguments: { requestId: cmd.requestId, pages: results },
});
log.info(
Expand Down Expand Up @@ -3182,7 +3193,7 @@ async function savePdf(): Promise<void> {
const base64 = uint8ArrayToBase64(pdfBytes);

const result = await app.callServerTool({
name: "save_pdf",
name: toolName("save_pdf"),
arguments: { url: pdfUrl, data: base64 },
});

Expand Down Expand Up @@ -4339,7 +4350,7 @@ async function fetchChunk(
const request = (async (): Promise<RangeResult> => {
try {
const result = await app.callServerTool({
name: "read_pdf_bytes",
name: toolName("read_pdf_bytes"),
arguments: { url, offset: begin, byteCount: end - begin },
});

Expand Down Expand Up @@ -4946,7 +4957,7 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
log.error("get_pages failed — submitting empty result:", err);
await app
.callServerTool({
name: "submit_page_data",
name: toolName("submit_page_data"),
arguments: { requestId: cmd.requestId, pages: [] },
})
.catch(() => {});
Expand All @@ -4959,15 +4970,15 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
const pdfBytes = await getAnnotatedPdfBytes();
const base64 = uint8ArrayToBase64(pdfBytes);
await app.callServerTool({
name: "submit_save_data",
name: toolName("submit_save_data"),
arguments: { requestId: cmd.requestId, data: base64 },
});
log.info(`save_as: submitted ${pdfBytes.length} bytes`);
} catch (err) {
log.error("save_as: failed to build bytes — submitting error:", err);
await app
.callServerTool({
name: "submit_save_data",
name: toolName("submit_save_data"),
arguments: {
requestId: cmd.requestId,
error: err instanceof Error ? err.message : String(err),
Expand All @@ -4984,7 +4995,7 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
log.error("get_viewer_state failed — submitting error:", err);
await app
.callServerTool({
name: "submit_viewer_state",
name: toolName("submit_viewer_state"),
arguments: {
requestId: cmd.requestId,
error: err instanceof Error ? err.message : String(err),
Expand Down Expand Up @@ -5058,7 +5069,7 @@ async function pollLoop(): Promise<void> {
while (polling && viewUUID) {
try {
const result = await app.callServerTool({
name: "poll_pdf_commands",
name: toolName("poll_pdf_commands"),
arguments: { viewUUID },
});
if (result.isError) {
Expand Down
29 changes: 29 additions & 0 deletions examples/pdf-server/src/tool-names.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, test } from "bun:test";
import { resolveToolName } from "./tool-names";

describe("resolveToolName", () => {
test("applies the host's prefix to other tools", () => {
expect(resolveToolName("save_pdf", "pdf-server-display_pdf")).toBe(
"pdf-server-save_pdf",
);
expect(resolveToolName("poll_pdf_commands", "a__b__display_pdf")).toBe(
"a__b__poll_pdf_commands",
);
});

test("keeps the bare name when the host does not rewrite names", () => {
expect(resolveToolName("save_pdf", "display_pdf")).toBe("save_pdf");
});

test("keeps the bare name when there is no host context", () => {
expect(resolveToolName("save_pdf", undefined)).toBe("save_pdf");
expect(resolveToolName("save_pdf", null)).toBe("save_pdf");
expect(resolveToolName("save_pdf", "")).toBe("save_pdf");
});

test("keeps the bare name when the invoked tool is not the entry tool", () => {
expect(resolveToolName("save_pdf", "other-server-get-time")).toBe(
"save_pdf",
);
});
});
27 changes: 27 additions & 0 deletions examples/pdf-server/src/tool-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Resolve the name a widget must use to call one of this server's own tools.
*
* A host that aggregates several MCP servers behind one session (mcphub, for
* example) can qualify tool names to avoid collisions, so `save_pdf` becomes
* `pdf-server-save_pdf`. `callServerTool` must then use the qualified name, but
* the widget only knows the bare one. The host does tell us the name it
* invoked the entry tool under (`toolInfo.tool.name`), so the prefix it added
* to the entry tool can be recovered and applied to every other tool.
*
* Falls back to the bare name when the host does not rewrite names, or when
* the invoked name is not this server's entry tool.
*
* See https://github.com/modelcontextprotocol/ext-apps/issues/753
*/

/** The only tool that mounts the viewer (the one with `_meta.ui.resourceUri`). */
export const ENTRY_TOOL = "display_pdf";

export function resolveToolName(
baseName: string,
invokedName: string | undefined | null,
entryTool: string = ENTRY_TOOL,
): string {
if (!invokedName || !invokedName.endsWith(entryTool)) return baseName;
return invokedName.slice(0, invokedName.length - entryTool.length) + baseName;
}