inspector/test-servers/src/test-server-fixtures.ts
cliffhall cfbdfdf2bd Merge remote-tracking branch 'origin/v2/main' into 1579-wave-1
# Conflicts:
#	AGENTS.md
#	clients/cli/README.md
#	clients/web/src/App.test.tsx
#	clients/web/src/App.tsx
#	clients/web/src/test/core/auth/utils.test.ts
#	core/mcp/node/transport.ts
#	test-servers/src/composable-test-server.ts
2026-07-03 17:39:07 -04:00

2141 lines
64 KiB
TypeScript

/**
* Shared test fixtures for composable MCP test servers
*
* This module provides helper functions for creating test tools, prompts, and resources.
* For the core composable server types and createMcpServer function, see composable-test-server.ts
*/
import * as z from "zod/v4";
import type { Implementation } from "@modelcontextprotocol/sdk/types.js";
import {
CreateMessageResultSchema,
CreateTaskResultSchema,
ElicitResultSchema,
GetTaskResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import type {
ToolDefinition,
TaskToolDefinition,
ResourceDefinition,
PromptDefinition,
ResourceTemplateDefinition,
ServerConfig,
TestServerContext,
} from "./composable-test-server.js";
import { getTestServerControl } from "./test-server-control.js";
import type {
ElicitRequestFormParams,
ElicitRequestURLParams,
} from "@modelcontextprotocol/sdk/types.js";
import type {
TaskRequestHandlerExtra,
CreateTaskRequestHandlerExtra,
} from "@modelcontextprotocol/sdk/experimental/tasks/interfaces.js";
import { RELATED_TASK_META_KEY } from "@modelcontextprotocol/sdk/types.js";
import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
import type { ShapeOutput } from "@modelcontextprotocol/sdk/server/zod-compat.js";
import type {
GetTaskResult,
CallToolResult,
ServerRequest,
ServerNotification,
} from "@modelcontextprotocol/sdk/types.js";
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
import type { ZodRawShapeCompat } from "@modelcontextprotocol/sdk/server/zod-compat.js";
/** Build a CallToolResult from a text message (and optional isError). */
function toToolResult(text: string, isError?: boolean): CallToolResult {
return {
content: [{ type: "text", text }],
...(isError && { isError: true }),
};
}
// Re-export types and functions from composable-test-server for backward compatibility
export type {
ToolDefinition,
TaskToolDefinition,
ResourceDefinition,
PromptDefinition,
ResourceTemplateDefinition,
ServerConfig,
} from "./composable-test-server.js";
export { createMcpServer } from "./composable-test-server.js";
/**
* Create multiple numbered tools for pagination testing
* @param count Number of tools to create
* @returns Array of tool definitions
*/
export function createNumberedTools(count: number): ToolDefinition[] {
const tools: ToolDefinition[] = [];
for (let i = 1; i <= count; i++) {
tools.push({
name: `tool_${i}`,
description: `Test tool number ${i}`,
inputSchema: {
message: z.string().describe(`Message for tool ${i}`),
},
handler: async (params: Record<string, unknown>) => {
return toToolResult(`Tool ${i}: ${params.message as string}`);
},
});
}
return tools;
}
/**
* Create multiple numbered resources for pagination testing
* @param count Number of resources to create
* @returns Array of resource definitions
*/
export function createNumberedResources(count: number): ResourceDefinition[] {
const resources: ResourceDefinition[] = [];
for (let i = 1; i <= count; i++) {
resources.push({
name: `resource_${i}`,
uri: `test://resource_${i}`,
description: `Test resource number ${i}`,
mimeType: "text/plain",
text: `Content for resource ${i}`,
});
}
return resources;
}
/**
* Create multiple numbered resource templates for pagination testing
* @param count Number of resource templates to create
* @returns Array of resource template definitions
*/
export function createNumberedResourceTemplates(
count: number,
): ResourceTemplateDefinition[] {
const templates: ResourceTemplateDefinition[] = [];
for (let i = 1; i <= count; i++) {
templates.push({
name: `template_${i}`,
uriTemplate: `test://template_${i}/{param}`,
description: `Test resource template number ${i}`,
handler: async (uri: URL, variables: Record<string, unknown>) => {
return {
contents: [
{
uri: uri.toString(),
mimeType: "text/plain",
text: `Content for template ${i} with param ${variables.param}`,
},
],
};
},
});
}
return templates;
}
/**
* Create multiple numbered prompts for pagination testing
* @param count Number of prompts to create
* @returns Array of prompt definitions
*/
export function createNumberedPrompts(count: number): PromptDefinition[] {
const prompts: PromptDefinition[] = [];
for (let i = 1; i <= count; i++) {
prompts.push({
name: `prompt_${i}`,
description: `Test prompt number ${i}`,
promptString: `This is prompt ${i}`,
});
}
return prompts;
}
/**
* Create an "echo" tool that echoes back the input message
*/
export function createEchoTool(): ToolDefinition {
return {
name: "echo",
description: "Echo back the input message",
inputSchema: {
message: z.string().describe("Message to echo back"),
},
handler: async (
params: Record<string, unknown>,
_context?: TestServerContext,
) => {
return toToolResult(`Echo: ${params.message as string}`);
},
};
}
/**
* Create a "get-env" tool matching @modelcontextprotocol/server-everything.
* Returns the server process environment as pretty-printed JSON text.
*/
export function createGetEnvTool(): ToolDefinition {
return {
name: "get-env",
description:
"Returns all environment variables, helpful for debugging MCP server configuration",
inputSchema: {},
handler: async () => {
return toToolResult(JSON.stringify(process.env, null, 2));
},
};
}
/**
* Create a tool that writes a message to stderr. Used to test stderr capture/piping.
*/
export function createWriteToStderrTool(): ToolDefinition {
return {
name: "write_to_stderr",
description: "Write a message to stderr (for testing stderr capture)",
inputSchema: {
message: z.string().describe("Message to write to stderr"),
},
handler: async (params: Record<string, unknown>) => {
const msg = params.message as string;
process.stderr.write(`${msg}\n`);
return toToolResult(`Wrote to stderr: ${msg}`);
},
};
}
/**
* Create an "add" tool that adds two numbers together
*/
export function createAddTool(): ToolDefinition {
return {
name: "add",
description: "Add two numbers together",
inputSchema: {
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
},
handler: async (
params: Record<string, unknown>,
_context?: TestServerContext,
) => {
const a = params.a as number;
const b = params.b as number;
return toToolResult(JSON.stringify({ result: a + b }));
},
};
}
/**
* Create a "get_sum" tool that returns the sum of two numbers (alias for add)
*/
export function createGetSumTool(): ToolDefinition {
return {
name: "get_sum",
description: "Get the sum of two numbers",
inputSchema: {
a: z.number().describe("First number"),
b: z.number().describe("Second number"),
},
handler: async (
params: Record<string, unknown>,
_context?: TestServerContext,
) => {
const a = params.a as number;
const b = params.b as number;
return toToolResult(JSON.stringify({ result: a + b }));
},
};
}
/**
* Create a "collect_sample" tool that sends a sampling request and returns the response
*/
export function createCollectSampleTool(): ToolDefinition {
return {
name: "collect_sample",
description:
"Send a sampling request with the given text and return the response",
inputSchema: {
text: z.string().describe("Text to send in the sampling request"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
): Promise<CallToolResult> => {
if (!context) {
throw new Error("Server context not available");
}
const server = context.server;
const text = params.text as string;
// Send a sampling/createMessage request to the client using the SDK's createMessage method
try {
const result = await server.server.createMessage({
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: text,
},
},
],
maxTokens: 100, // Required parameter
});
return toToolResult(`Sampling response: ${JSON.stringify(result)}`);
} catch (error) {
console.error(
"[collect_sample] Error sending/receiving sampling request:",
error,
);
throw error;
}
},
};
}
/**
* Create a "list_roots" tool that calls roots/list and returns the roots
*/
export function createListRootsTool(): ToolDefinition {
return {
name: "list_roots",
description: "List the current roots configured on the client",
inputSchema: {},
handler: async (
_params: Record<string, unknown>,
context?: TestServerContext,
): Promise<CallToolResult> => {
if (!context) {
throw new Error("Server context not available");
}
const server = context.server;
try {
// Call roots/list on the client using the SDK's listRoots method
const result = await server.server.listRoots();
return toToolResult(`Roots: ${JSON.stringify(result.roots, null, 2)}`);
} catch (error) {
return toToolResult(
`Error listing roots: ${error instanceof Error ? error.message : String(error)}`,
true,
);
}
},
};
}
/**
* Create a "collectElicitation" tool that sends an elicitation request and returns the response
*/
export function createCollectFormElicitationTool(): ToolDefinition {
return {
name: "collect_elicitation",
description:
"Send an elicitation request with the given message and schema and return the response",
inputSchema: {
message: z
.string()
.describe("Message to send in the elicitation request"),
schema: z.unknown().describe("JSON schema for the elicitation request"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
): Promise<CallToolResult> => {
if (!context) {
throw new Error("Server context not available");
}
const server = context.server;
const message = params.message as string;
const schema =
params.schema as ElicitRequestFormParams["requestedSchema"];
// Send a form-based elicitation request using the SDK's elicitInput method
try {
const elicitationParams: ElicitRequestFormParams = {
message,
requestedSchema: schema,
};
const result = await server.server.elicitInput(elicitationParams);
return toToolResult(`Elicitation response: ${JSON.stringify(result)}`);
} catch (error) {
console.error(
"[collectElicitation] Error sending/receiving elicitation request:",
error,
);
throw error;
}
},
};
}
/**
* Create a "url_elicitation_form" tool that spins up a simple HTTP server on a dynamic
* port with a form page, sends that URL via URL elicitation, and on form submit collects
* the text input, includes it in the tool response, and closes the server.
*/
export function createUrlElicitationFormTool(): ToolDefinition {
return {
name: "url_elicitation_form",
description:
"Present a form via URL elicitation; collects submitted text and returns it in the tool response",
inputSchema: {
message: z
.string()
.optional()
.describe(
"Message to show in the elicitation (default: prompt for input)",
),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
): Promise<CallToolResult> => {
if (!context) {
throw new Error("Server context not available");
}
const server = context.server;
const message =
(params.message as string) || "Please submit a value in the form";
const elicitationId = `url-form-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
let resolveFormData!: (value: string) => void;
const formDataPromise = new Promise<string>((resolve) => {
resolveFormData = resolve;
});
const completionNotifier =
server.server.createElicitationCompletionNotifier(elicitationId);
const { createServer } = await import("node:http");
const { createServer: createNetServer } = await import("node:net");
const formHtml = (elicitationId: string) => `
<!DOCTYPE html>
<html>
<head><title>Submit Value</title></head>
<body>
<form method="POST" action="/">
<input type="hidden" name="elicitation" value="${elicitationId}" />
<label>Value: <input type="text" name="value" required /></label>
<button type="submit">Submit</button>
</form>
</body>
</html>`;
const successHtml = `
<!DOCTYPE html>
<html>
<head><title>Submitted</title></head>
<body><p>Submitted. You can close this window.</p></body>
</html>`;
const httpServer = createServer((req, res) => {
if (req.method === "GET" && req.url === "/") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end(formHtml(elicitationId));
return;
}
if (req.method === "POST" && req.url === "/") {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", () => {
const params = new URLSearchParams(body);
const value = params.get("value") ?? "";
completionNotifier().catch(() => {});
resolveFormData(value);
httpServer.close();
res.writeHead(200, { "Content-Type": "text/html" });
res.end(successHtml);
});
return;
}
res.writeHead(404);
res.end();
});
const port = await new Promise<number>((resolve, reject) => {
const s = createNetServer();
s.listen(0, "127.0.0.1", () => {
const addr = s.address() as { port: number };
s.close(() => resolve(addr.port));
});
s.on("error", reject);
});
httpServer.listen(port, "127.0.0.1");
const url = `http://127.0.0.1:${port}/`;
try {
const result = await server.server.elicitInput({
mode: "url",
message,
elicitationId,
url,
});
if (result.action !== "accept") {
httpServer.close();
return toToolResult(
`Elicitation ${result.action}: user did not accept`,
);
}
const collectedValue = await formDataPromise;
return toToolResult(`Collected value: ${collectedValue}`);
} catch (error) {
httpServer.close();
throw error;
}
},
};
}
/**
* Create a "collect_url_elicitation" tool that sends a URL-based elicitation request
* to the client and returns the response
*/
export function createCollectUrlElicitationTool(): ToolDefinition {
return {
name: "collect_url_elicitation",
description:
"Send a URL-based elicitation request with the given message and URL and return the response",
inputSchema: {
message: z
.string()
.describe("Message to send in the elicitation request"),
url: z.string().url().describe("URL for the user to navigate to"),
elicitationId: z
.string()
.optional()
.describe("Optional elicitation ID (generated if not provided)"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
): Promise<CallToolResult> => {
if (!context) {
throw new Error("Server context not available");
}
const server = context.server;
const message = params.message as string;
const url = params.url as string;
const elicitationId =
(params.elicitationId as string) ||
`url-elicitation-${Date.now()}-${Math.random()}`;
// Send a URL-based elicitation request using the SDK's elicitInput method
try {
const elicitationParams: ElicitRequestURLParams = {
mode: "url",
message,
elicitationId,
url,
};
const result = await server.server.elicitInput(elicitationParams);
return toToolResult(
`URL elicitation response: ${JSON.stringify(result)}`,
);
} catch (error) {
console.error(
"[collect_url_elicitation] Error sending/receiving URL elicitation request:",
error,
);
throw error;
}
},
};
}
/**
* Create a "send_notification" tool that sends a notification message from the server
*/
export function createSendNotificationTool(): ToolDefinition {
return {
name: "send_notification",
description: "Send a notification message from the server",
inputSchema: {
message: z.string().describe("Notification message to send"),
level: z
.enum([
"debug",
"info",
"notice",
"warning",
"error",
"critical",
"alert",
"emergency",
])
.optional()
.describe("Log level for the notification"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
): Promise<CallToolResult> => {
if (!context) {
throw new Error("Server context not available");
}
const server = context.server;
const message = params.message as string;
const level = (params.level as string) || "info";
// Send a notification from the server
// Notifications don't have an id and use the jsonrpc format
try {
await server.server.notification({
method: "notifications/message",
params: {
level,
logger: "test-server",
data: {
message,
},
},
});
return toToolResult(`Notification sent: ${message}`);
} catch (error) {
console.error("[send_notification] Error sending notification:", error);
throw error;
}
},
};
}
/**
* Create a "get-annotated-message" tool that returns a message with optional image
*/
export function createGetAnnotatedMessageTool(): ToolDefinition {
return {
name: "get_annotated_message",
description: "Get an annotated message",
inputSchema: {
messageType: z
.enum(["success", "error", "warning", "info"])
.describe("Type of message"),
includeImage: z
.boolean()
.optional()
.describe("Whether to include an image"),
},
handler: async (
params: Record<string, unknown>,
_context?: TestServerContext,
): Promise<CallToolResult> => {
const messageType = params.messageType as string;
const includeImage = params.includeImage as boolean | undefined;
const message = `This is a ${messageType} message`;
const content: Array<
| { type: "text"; text: string }
| { type: "image"; data: string; mimeType: string }
> = [
{
type: "text",
text: message,
},
];
if (includeImage) {
content.push({
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", // 1x1 transparent PNG
mimeType: "image/png",
});
}
return { content };
},
};
}
/** Output schema for get_temp: temperature, unit, city */
const GetTempOutputSchema = z.object({
temperature: z.number().describe("Temperature value"),
unit: z.string().describe("C or F"),
city: z.string().describe("City name"),
});
/**
* Create a "get_temp" tool that returns both content (human-readable) and structuredContent (schema-validated).
* Takes city and units (C/F), returns mock temperature 25 and matching text + structured output.
*/
export function createGetTempTool(): ToolDefinition {
return {
name: "get_temp",
description:
"Get the current temperature for a city (mock; returns 25 in requested units)",
inputSchema: {
city: z.string().describe("City name"),
units: z.enum(["C", "F"]).describe("Temperature units"),
},
outputSchema: GetTempOutputSchema,
handler: async (params: Record<string, unknown>) => {
const city = (params.city as string) || "Unknown";
const unit = (params.units as "C" | "F") || "C";
const temperature = 25;
const text = `The temperature in ${city} is ${temperature} degrees ${unit}`;
return {
content: [{ type: "text" as const, text }],
structuredContent: { temperature, unit, city },
};
},
};
}
/**
* Create a "get_temp_extra" tool that declares the same output schema as
* get_temp but returns an EXTRA, undeclared property in structuredContent.
*
* The SDK server validates output via zod safeParse (which strips unknown keys
* and passes), then sends the ORIGINAL structuredContent — so the extra key
* reaches the wire. The SDK *client* validates that payload against the strict
* JSON schema derived from the output schema and rejects it
* ("must NOT have additional properties"). This mirrors real-world servers
* (e.g. MCP App tools) whose results legacy hosts render but whose strict
* client validation otherwise denies. Used to exercise
* InspectorClient.callTool's `skipOutputValidation` option.
*/
export function createGetTempExtraTool(): ToolDefinition {
return {
name: "get_temp_extra",
description:
"Like get_temp, but returns an extra structuredContent property that violates the output schema (for validation-bypass testing)",
inputSchema: {
city: z.string().describe("City name"),
units: z.enum(["C", "F"]).describe("Temperature units"),
},
outputSchema: GetTempOutputSchema,
handler: async (params: Record<string, unknown>) => {
const city = (params.city as string) || "Unknown";
const unit = (params.units as "C" | "F") || "C";
const temperature = 25;
const text = `The temperature in ${city} is ${temperature} degrees ${unit}`;
return {
content: [{ type: "text" as const, text }],
// `extra` is not in GetTempOutputSchema → strict client validation fails.
structuredContent: { temperature, unit, city, extra: "undeclared" },
};
},
};
}
/**
* Create a "simple_prompt" prompt definition
*/
export function createSimplePrompt(): PromptDefinition {
return {
name: "simple_prompt",
description: "A simple prompt for testing",
promptString: "This is a simple prompt for testing purposes.",
};
}
/**
* Create an "args_prompt" prompt that accepts arguments
*/
export function createArgsPrompt(
completions?: Record<
string,
(
argumentValue: string,
context?: Record<string, string>,
) => Promise<string[]> | string[]
>,
): PromptDefinition {
return {
name: "args_prompt",
description: "A prompt that accepts arguments for testing",
promptString: "This is a prompt with arguments: city={city}, state={state}",
argsSchema: {
city: z.string().describe("City name"),
state: z.string().describe("State name"),
},
completions,
};
}
/** Canonical URI for the {@link createMcpAppDemoResource} UI resource, referenced by {@link createMcpAppDemoTool}'s `_meta.ui.resourceUri`. Exported so tests can assert against it without redefining the literal. */
export const MCP_APP_DEMO_URI = "ui://demo/widget.html";
/**
* Minimal MCP App widget that exercises the host-side UI protocol surface
* (size-changed, ui/message, log notification, host-context render). Kept as a
* single inline HTML string with no external scripts so the sandbox CSP's
* locked-down defaults are sufficient.
*
* The lone `rgba(0,0,0,0.06)` is a deliberate exception to the AGENTS.md
* color-token rule: this is a self-contained static fixture served into the
* sandbox iframe, not a Mantine component, so the `--inspector-*` CSS custom
* properties (defined in the web client's App.css) are not in scope here.
*/
const MCP_APP_DEMO_HTML = `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>mcp-app-demo widget</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; padding: 16px; }
pre { background: rgba(0,0,0,0.06); padding: 8px; border-radius: 6px; }
</style>
</head>
<body>
<h2 id="title">mcp-app-demo</h2>
<pre id="ctx">waiting for ui/initialize…</pre>
<script type="module">
// Per spec, ui/initialize is a View→Host REQUEST: the view sends it on
// load and the host responds with hostContext + capabilities. The view
// then sends notifications/initialized once it has rendered.
// Production apps should use @modelcontextprotocol/ext-apps (the App
// class handles the handshake and origin discipline internally) — this
// fixture is a minimal no-SDK demo of the raw protocol for testing the
// host side.
const INIT_ID = 1;
let nextId = 2;
// Captured from the host's ui/initialize RESPONSE; thereafter every send
// targets, and every receive is checked against, this exact origin so a
// sibling frame on a different origin cannot inject or intercept traffic.
let HOST_ORIGIN = null;
const send = (msg) =>
window.parent.postMessage(
{ jsonrpc: "2.0", ...msg },
HOST_ORIGIN ?? "*",
);
let lastCtx = {};
const renderCtx = (patch) => {
// host-context-changed carries a PARTIAL (only changed fields per
// spec); merge into the running snapshot so unchanged fields persist.
lastCtx = { ...lastCtx, ...patch };
document.getElementById("ctx").textContent = JSON.stringify(
{
theme: lastCtx.theme,
displayMode: lastCtx.displayMode,
containerDimensions: lastCtx.containerDimensions,
},
null,
2,
);
};
const onInitialized = (hostContext) => {
renderCtx(hostContext);
// Signal the view is ready (host gates view→host requests on this).
send({ method: "ui/notifications/initialized" });
// Standard MCP log notification — surfaced by the host's log panel.
send({
method: "notifications/message",
params: { level: "info", data: "mcp-app-demo initialized" },
});
// Tell the host the rendered content height.
send({
method: "ui/notifications/size-changed",
params: {
width: document.body.clientWidth,
height: document.body.scrollHeight,
},
});
// Submit one user-role message via ui/message.
send({
id: nextId++,
method: "ui/message",
params: {
content: [{ type: "text", text: "hello from mcp-app-demo" }],
},
});
};
window.addEventListener("message", (ev) => {
if (HOST_ORIGIN !== null && ev.origin !== HOST_ORIGIN) return;
const m = ev.data;
if (!m || m.jsonrpc !== "2.0") return;
if (m.id === INIT_ID && m.result) {
HOST_ORIGIN = ev.origin;
onInitialized(m.result.hostContext);
} else if (m.method === "ui/notifications/host-context-changed") {
// params IS the partial McpUiHostContext (spec.types.d.ts:290).
renderCtx(m.params);
}
});
// Kick off the handshake.
send({
id: INIT_ID,
method: "ui/initialize",
params: {
protocolVersion: "2026-01-26",
appInfo: { name: "mcp-app-demo", version: "1.0.0" },
appCapabilities: {},
},
});
</script>
</body>
</html>`;
/**
* Tool definition for the MCP App demo. Carries `_meta.ui.resourceUri` so
* clients recognize it as an App tool; the call result echoes the input title
* so the rendered widget can be visually correlated with the call.
*/
export function createMcpAppDemoTool(): ToolDefinition {
return {
name: "mcp_app_demo",
description:
"Render a minimal MCP App widget that exercises size-changed, ui/message, logging, and host-context rendering.",
inputSchema: {
title: z.string().describe("Heading shown in the rendered widget"),
},
_meta: {
ui: { resourceUri: MCP_APP_DEMO_URI, visibility: ["model", "app"] },
},
handler: async (params: Record<string, unknown>) => {
return toToolResult(
`mcp_app_demo rendered with title="${String(params.title)}"`,
);
},
};
}
/**
* UI resource for {@link createMcpAppDemoTool}. Declares a permissive
* `_meta.ui.csp` (no external connect/resource domains) and a sample
* `permissions` block so `--app-info` and the host's CSP enforcement both have
* something to read.
*/
export function createMcpAppDemoResource(): ResourceDefinition {
return {
name: "mcp_app_demo_widget",
uri: MCP_APP_DEMO_URI,
description: "Inline HTML widget for the mcp_app_demo tool",
mimeType: "text/html",
text: MCP_APP_DEMO_HTML,
_meta: {
ui: {
csp: { connectDomains: [], resourceDomains: [] },
permissions: { clipboard: false },
prefersBorder: true,
},
},
};
}
/**
* Create an "architecture" resource definition
*/
export function createArchitectureResource(): ResourceDefinition {
return {
name: "architecture",
uri: "demo://resource/static/document/architecture.md",
description: "Architecture documentation",
mimeType: "text/markdown",
text: `# Architecture Documentation
This is a test resource for the MCP test server.
## Overview
This resource is used for testing resource reading functionality in the CLI.
## Sections
- Introduction
- Design
- Implementation
- Testing
## Notes
This is a static resource provided by the test MCP server.
`,
};
}
/**
* Create a "test_cwd" resource that exposes the current working directory (generally useful when testing with the stdio test server)
*/
export function createTestCwdResource(): ResourceDefinition {
return {
name: "test_cwd",
uri: "test://cwd",
description: "Current working directory of the test server",
mimeType: "text/plain",
text: process.cwd(),
};
}
/**
* Create a "test_env" resource that exposes environment variables (generally useful when testing with the stdio test server)
*/
export function createTestEnvResource(): ResourceDefinition {
return {
name: "test_env",
uri: "test://env",
description: "Environment variables available to the test server",
mimeType: "application/json",
text: JSON.stringify(process.env, null, 2),
};
}
/**
* Create a "test_argv" resource that exposes command-line arguments (generally useful when testing with the stdio test server)
*/
export function createTestArgvResource(): ResourceDefinition {
return {
name: "test_argv",
uri: "test://argv",
description: "Command-line arguments the test server was started with",
mimeType: "application/json",
text: JSON.stringify(process.argv, null, 2),
};
}
/**
* Create minimal server info for test servers
*/
export function createTestServerInfo(
name: string = "test-server",
version: string = "1.0.0",
): Implementation {
return {
name,
version,
};
}
/**
* Create a "file" resource template that reads files by path
*/
export function createFileResourceTemplate(
completionCallback?: (
argumentName: string,
value: string,
context?: Record<string, string>,
) => Promise<string[]> | string[],
listCallback?: () => Promise<string[]> | string[],
): ResourceTemplateDefinition {
return {
name: "file",
uriTemplate: "file:///{path}",
description: "Read a file by path",
inputSchema: {
path: z.string().describe("File path to read"),
},
handler: async (uri: URL, params: Record<string, unknown>) => {
const path = params.path as string;
// For testing, return a mock file content
return {
contents: [
{
uri: uri.toString(),
mimeType: "text/plain",
text: `Mock file content for: ${path}\nThis is a test resource template.`,
},
],
};
},
complete: completionCallback,
list: listCallback,
};
}
/**
* Create a "user" resource template that returns user data by ID
*/
export function createUserResourceTemplate(
completionCallback?: (
argumentName: string,
value: string,
context?: Record<string, string>,
) => Promise<string[]> | string[],
listCallback?: () => Promise<string[]> | string[],
): ResourceTemplateDefinition {
return {
name: "user",
uriTemplate: "user://{userId}",
description: "Get user data by ID",
inputSchema: {
userId: z.string().describe("User ID"),
},
handler: async (uri: URL, params: Record<string, unknown>) => {
const userId = params.userId as string;
return {
contents: [
{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(
{
id: userId,
name: `User ${userId}`,
email: `user${userId}@example.com`,
role: "test-user",
},
null,
2,
),
},
],
};
},
complete: completionCallback,
list: listCallback,
};
}
/**
* Create a tool that adds a resource to the server and sends list_changed notification
*/
export function createAddResourceTool(): ToolDefinition {
return {
name: "add_resource",
description:
"Add a resource to the server and send list_changed notification",
inputSchema: {
uri: z.string().describe("Resource URI"),
name: z.string().describe("Resource name"),
description: z.string().optional().describe("Resource description"),
mimeType: z.string().optional().describe("Resource MIME type"),
text: z.string().optional().describe("Resource text content"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
) => {
if (!context) {
throw new Error("Server context not available");
}
const { server, state } = context;
// Register with SDK (returns RegisteredResource)
const registered = server.registerResource(
params.name as string,
params.uri as string,
{
description: params.description as string | undefined,
mimeType: params.mimeType as string | undefined,
},
async () => {
return {
contents: params.text
? [
{
uri: params.uri as string,
mimeType: params.mimeType as string | undefined,
text: params.text as string,
},
]
: [],
};
},
);
// Track in state (keyed by URI)
state.registeredResources.set(params.uri as string, registered);
// Send notification if capability enabled
if (state.listChangedConfig.resources) {
server.sendResourceListChanged();
}
return toToolResult(`Resource ${params.uri} added`);
},
};
}
/**
* Create a tool that removes a resource from the server by URI and sends list_changed notification
*/
export function createRemoveResourceTool(): ToolDefinition {
return {
name: "remove_resource",
description:
"Remove a resource from the server by URI and send list_changed notification",
inputSchema: {
uri: z.string().describe("Resource URI to remove"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
) => {
if (!context) {
throw new Error("Server context not available");
}
const { server, state } = context;
// Find registered resource by URI
const resource = state.registeredResources.get(params.uri as string);
if (!resource) {
throw new Error(`Resource with URI ${params.uri} not found`);
}
// Remove from SDK registry
resource.remove();
// Remove from tracking
state.registeredResources.delete(params.uri as string);
// Send notification if capability enabled
if (state.listChangedConfig.resources) {
server.sendResourceListChanged();
}
return toToolResult(`Resource ${params.uri} removed`);
},
};
}
/**
* Create a tool that adds a tool to the server and sends list_changed notification
*/
export function createAddToolTool(): ToolDefinition {
return {
name: "add_tool",
description: "Add a tool to the server and send list_changed notification",
inputSchema: {
name: z.string().describe("Tool name"),
description: z.string().describe("Tool description"),
inputSchema: z.unknown().optional().describe("Tool input schema"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
) => {
if (!context) {
throw new Error("Server context not available");
}
const { server, state } = context;
// Register with SDK (returns RegisteredTool)
const registered = server.registerTool(
params.name as string,
{
description: params.description as string,
inputSchema: params.inputSchema as ZodRawShapeCompat | undefined,
},
async () => {
return {
content: [
{
type: "text" as const,
text: `Tool ${params.name} executed`,
},
],
};
},
);
// Track in state (keyed by name)
state.registeredTools.set(params.name as string, registered);
// Send notification if capability enabled
// Note: sendToolListChanged() is synchronous on McpServer but internally calls async Server method
// We don't await it, but the tool should be registered before sending the notification
if (state.listChangedConfig.tools) {
// Small delay to ensure tool is fully registered in SDK's internal state
await new Promise((resolve) => setTimeout(resolve, 10));
server.sendToolListChanged();
}
return toToolResult(`Tool ${params.name} added`);
},
};
}
/**
* Create a tool that removes a tool from the server by name and sends list_changed notification
*/
export function createRemoveToolTool(): ToolDefinition {
return {
name: "remove_tool",
description:
"Remove a tool from the server by name and send list_changed notification",
inputSchema: {
name: z.string().describe("Tool name to remove"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
) => {
if (!context) {
throw new Error("Server context not available");
}
const { server, state } = context;
// Find registered tool by name
const tool = state.registeredTools.get(params.name as string);
if (!tool) {
throw new Error(`Tool ${params.name} not found`);
}
// Remove from SDK registry
tool.remove();
// Remove from tracking
state.registeredTools.delete(params.name as string);
// Send notification if capability enabled
if (state.listChangedConfig.tools) {
server.sendToolListChanged();
}
return toToolResult(`Tool ${params.name} removed`);
},
};
}
/**
* Create a tool that adds a prompt to the server and sends list_changed notification
*/
export function createAddPromptTool(): ToolDefinition {
return {
name: "add_prompt",
description:
"Add a prompt to the server and send list_changed notification",
inputSchema: {
name: z.string().describe("Prompt name"),
description: z.string().optional().describe("Prompt description"),
promptString: z.string().describe("Prompt text"),
argsSchema: z.unknown().optional().describe("Prompt arguments schema"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
) => {
if (!context) {
throw new Error("Server context not available");
}
const { server, state } = context;
// Register with SDK (returns RegisteredPrompt)
const registered = server.registerPrompt(
params.name as string,
{
description: params.description as string | undefined,
argsSchema: params.argsSchema as ZodRawShapeCompat | undefined,
},
async () => {
return {
messages: [
{
role: "user" as const,
content: {
type: "text" as const,
text: params.promptString as string,
},
},
],
};
},
);
// Track in state (keyed by name)
state.registeredPrompts.set(params.name as string, registered);
// Send notification if capability enabled
if (state.listChangedConfig.prompts) {
server.sendPromptListChanged();
}
return toToolResult(`Prompt ${params.name} added`);
},
};
}
/**
* Create a tool that updates an existing resource's content and sends resource updated notification
*/
export function createUpdateResourceTool(): ToolDefinition {
return {
name: "update_resource",
description:
"Update an existing resource's content and send resource updated notification",
inputSchema: {
uri: z.string().describe("Resource URI to update"),
text: z.string().describe("New resource text content"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
) => {
if (!context) {
throw new Error("Server context not available");
}
const { server, state } = context;
// Find registered resource by URI
const resource = state.registeredResources.get(params.uri as string);
if (!resource) {
throw new Error(`Resource with URI ${params.uri} not found`);
}
// Get the current resource metadata to preserve mimeType
const currentResource = state.registeredResources.get(
params.uri as string,
);
const mimeType = currentResource?.metadata?.mimeType || "text/plain";
// Update the resource's callback to return new content
resource.update({
callback: async () => {
return {
contents: [
{
uri: params.uri as string,
mimeType,
text: params.text as string,
},
],
};
},
});
// Send resource updated notification only if subscribed
const uri = params.uri as string;
if (state.resourceSubscriptions.has(uri)) {
await server.server.sendResourceUpdated({
uri,
});
}
return toToolResult(`Resource ${params.uri} updated`);
},
};
}
/**
* Create a tool that sends progress notifications during execution
* @param name Tool name (default: "send_progress")
* @returns Tool definition
*/
export function createSendProgressTool(
name: string = "send_progress",
): ToolDefinition {
return {
name,
description:
"Send progress notifications during tool execution, then return a result",
inputSchema: {
units: z
.number()
.int()
.positive()
.describe("Number of progress units to send"),
delayMs: z
.number()
.int()
.nonnegative()
.default(100)
.describe("Delay in milliseconds between progress notifications"),
total: z
.number()
.int()
.positive()
.optional()
.describe("Total number of units (for percentage calculation)"),
message: z
.string()
.optional()
.describe("Progress message to include in notifications"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
extra?: RequestHandlerExtra<ServerRequest, ServerNotification>,
): Promise<CallToolResult> => {
if (!context) {
throw new Error("Server context not available");
}
const server = context.server;
const units = params.units as number;
const delayMs = (params.delayMs as number) || 100;
const total = params.total as number | undefined;
const message = (params.message as string) || "Processing...";
// Extract progressToken from metadata
const progressToken = extra?._meta?.progressToken;
// Send progress notifications
let sent = 0;
for (let i = 1; i <= units; i++) {
if (context.serverControl?.isClosing()) {
break;
}
// Wait before sending notification (except for the first one)
if (i > 1 && delayMs > 0) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
if (context.serverControl?.isClosing()) {
break;
}
if (progressToken !== undefined) {
const progressParams: {
progress: number;
total?: number;
message?: string;
progressToken: string | number;
} = {
progress: i,
message: `${message} (${i}/${units})`,
progressToken,
};
if (total !== undefined) {
progressParams.total = total;
}
try {
await server.server.notification(
{
method: "notifications/progress",
params: progressParams,
},
{ relatedRequestId: extra?.requestId },
);
sent = i;
} catch (error) {
console.error(
"[send_progress] Error sending progress notification:",
error,
);
break;
}
}
}
return toToolResult(
`Completed ${sent} progress notifications (units: ${sent}, total: ${total ?? units})`,
);
},
};
}
export function createRemovePromptTool(): ToolDefinition {
return {
name: "remove_prompt",
description:
"Remove a prompt from the server by name and send list_changed notification",
inputSchema: {
name: z.string().describe("Prompt name to remove"),
},
handler: async (
params: Record<string, unknown>,
context?: TestServerContext,
) => {
if (!context) {
throw new Error("Server context not available");
}
const { server, state } = context;
// Find registered prompt by name
const prompt = state.registeredPrompts.get(params.name as string);
if (!prompt) {
throw new Error(`Prompt ${params.name} not found`);
}
// Remove from SDK registry
prompt.remove();
// Remove from tracking
state.registeredPrompts.delete(params.name as string);
// Send notification if capability enabled
if (state.listChangedConfig.prompts) {
server.sendPromptListChanged();
}
return toToolResult(`Prompt ${params.name} removed`);
},
};
}
/** Options for creating an immediate (non-task) tool that completes after a delay */
export interface ImmediateToolOptions {
name?: string; // default: "flexibleTask"
delayMs?: number; // default: 1000
}
/** Options for creating a task tool (createTask + getTask + getTaskResult) with optional progress, elicitation, sampling, etc. */
export interface TaskToolOptions {
name?: string; // default: "flexibleTask"
taskSupport?: "required" | "optional"; // default: "required"
delayMs?: number; // default: 1000 (time before task completes)
progressUnits?: number; // If provided, send progress notifications
elicitationSchema?: z.ZodTypeAny; // If provided, require elicitation with this schema
samplingText?: string; // If provided, require sampling with this text
failAfterDelay?: number; // If set, task fails after this delay (ms)
cancelAfterDelay?: number; // If set, task cancels itself after this delay (ms)
/** If set, send params.task: { ttl } so the client creates a receiver task and returns { task } immediately */
receiverTaskTtl?: number;
}
/** Payload we receive from the client via tasks/result when using receiver-task mode */
interface ReceiverTaskPayload {
content: unknown;
isElicit?: boolean;
}
/**
* Poll the client for a receiver task until terminal, then fetch tasks/result.
* Used when the server sent a create (elicitation or sampling) with params.task and got { task } back.
*/
async function pollReceiverTaskPayload(
extra: CreateTaskRequestHandlerExtra,
clientTaskId: string,
resultSchema: z.ZodTypeAny,
isElicit: boolean,
): Promise<ReceiverTaskPayload | null> {
for (let i = 0; i < 50; i++) {
if (getTestServerControl()?.isClosing()) break;
const getRes = await extra.sendRequest(
{ method: "tasks/get", params: { taskId: clientTaskId } },
GetTaskResultSchema,
);
const status = (getRes as { status: string }).status;
if (
status === "completed" ||
status === "failed" ||
status === "cancelled"
) {
if (status === "completed") {
try {
const payload = await extra.sendRequest(
{ method: "tasks/result", params: { taskId: clientTaskId } },
resultSchema,
);
return {
content: (payload as { content?: unknown }).content,
isElicit: isElicit ? true : undefined,
};
} catch {
// tasks/result may fail if task failed
}
}
break;
}
await new Promise((r) => setTimeout(r, 100));
}
return null;
}
/** Params for the async task execution runner used by the task tool */
interface RunTaskExecutionParams {
task: { taskId: string };
extra: CreateTaskRequestHandlerExtra;
message?: string;
progressToken?: string | number;
options: TaskToolOptions;
}
/**
* Runs the task execution (input phase, progress, delay, fail/cancel, completion).
* Invoked fire-and-forget from createTask after creating the task.
*/
async function runTaskExecution(params: RunTaskExecutionParams): Promise<void> {
const { task, extra, message, progressToken, options } = params;
const {
delayMs = 1000,
progressUnits,
elicitationSchema,
samplingText,
failAfterDelay,
cancelAfterDelay,
receiverTaskTtl,
} = options;
let receiverTaskPayload: ReceiverTaskPayload | null = null;
try {
// --- Input phase: elicitation or sampling (optional receiver-task polling) ---
if (elicitationSchema) {
await extra.taskStore.updateTaskStatus(task.taskId, "input_required");
try {
const jsonSchema = toJsonSchemaCompat(
elicitationSchema,
) as ElicitRequestFormParams["requestedSchema"];
const elicitationParams: ElicitRequestFormParams = {
message: `Please provide input for task ${task.taskId}`,
requestedSchema: jsonSchema,
_meta: {
[RELATED_TASK_META_KEY]: { taskId: task.taskId },
},
...(receiverTaskTtl != null && { task: { ttl: receiverTaskTtl } }),
};
const elicitResponse = await extra.sendRequest(
{
method: "elicitation/create",
params: elicitationParams,
},
(receiverTaskTtl != null
? z.union([ElicitResultSchema, CreateTaskResultSchema])
: ElicitResultSchema) as typeof ElicitResultSchema,
);
const elicitWithTask = elicitResponse as unknown as {
task?: { taskId: string };
};
if (receiverTaskTtl != null && elicitWithTask?.task) {
receiverTaskPayload =
(await pollReceiverTaskPayload(
extra,
elicitWithTask.task.taskId,
ElicitResultSchema,
true,
)) ?? null;
}
await extra.taskStore.updateTaskStatus(task.taskId, "working");
} catch (error) {
console.error("[flexibleTask] Elicitation error:", error);
await extra.taskStore.updateTaskStatus(
task.taskId,
"failed",
error instanceof Error ? error.message : String(error),
);
return;
}
}
if (samplingText) {
await extra.taskStore.updateTaskStatus(task.taskId, "input_required");
try {
const samplingResponse = await extra.sendRequest(
{
method: "sampling/createMessage",
params: {
messages: [
{
role: "user",
content: { type: "text", text: samplingText },
},
],
maxTokens: 100,
_meta: {
[RELATED_TASK_META_KEY]: { taskId: task.taskId },
},
...(receiverTaskTtl != null && {
task: { ttl: receiverTaskTtl },
}),
},
},
(receiverTaskTtl != null
? z.union([CreateMessageResultSchema, CreateTaskResultSchema])
: CreateMessageResultSchema) as typeof CreateMessageResultSchema,
);
const samplingWithTask = samplingResponse as unknown as {
task?: { taskId: string };
};
if (receiverTaskTtl != null && samplingWithTask?.task) {
receiverTaskPayload =
(await pollReceiverTaskPayload(
extra,
samplingWithTask.task.taskId,
CreateMessageResultSchema,
false,
)) ?? null;
}
await extra.taskStore.updateTaskStatus(task.taskId, "working");
} catch (error) {
console.error("[flexibleTask] Sampling error:", error);
await extra.taskStore.updateTaskStatus(
task.taskId,
"failed",
error instanceof Error ? error.message : String(error),
);
return;
}
}
// --- Progress or delay ---
if (
progressUnits !== undefined &&
progressUnits > 0 &&
progressToken !== undefined
) {
for (let i = 1; i <= progressUnits; i++) {
if (getTestServerControl()?.isClosing()) break;
await new Promise((resolve) =>
setTimeout(resolve, delayMs / progressUnits),
);
if (getTestServerControl()?.isClosing()) break;
try {
await extra.sendNotification({
method: "notifications/progress",
params: {
progress: i,
total: progressUnits,
message: `Processing... ${i}/${progressUnits}`,
progressToken,
_meta: {
[RELATED_TASK_META_KEY]: { taskId: task.taskId },
},
},
});
} catch (error) {
console.error("[flexibleTask] Progress notification error:", error);
break;
}
}
} else {
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
// --- Optional fail/cancel ---
if (failAfterDelay !== undefined) {
await new Promise((resolve) => setTimeout(resolve, failAfterDelay));
await extra.taskStore.updateTaskStatus(
task.taskId,
"failed",
"Task failed as configured",
);
return;
}
if (cancelAfterDelay !== undefined) {
await new Promise((resolve) => setTimeout(resolve, cancelAfterDelay));
await extra.taskStore.updateTaskStatus(task.taskId, "cancelled");
return;
}
// --- Complete with stored or default result ---
const result =
receiverTaskPayload?.content != null
? receiverTaskPayload.isElicit
? {
content: [
{
type: "text" as const,
text: JSON.stringify(receiverTaskPayload.content),
},
],
}
: {
content: Array.isArray(receiverTaskPayload.content)
? receiverTaskPayload.content
: [receiverTaskPayload.content],
}
: {
content: [
{
type: "text",
text: JSON.stringify({
message: `Task completed: ${message || "no message"}`,
taskId: task.taskId,
}),
},
],
};
await extra.taskStore.storeTaskResult(task.taskId, "completed", result);
await extra.taskStore.updateTaskStatus(task.taskId, "completed");
} catch (error) {
try {
const currentTask = await extra.taskStore.getTask(task.taskId);
if (
currentTask &&
currentTask.status !== "completed" &&
currentTask.status !== "failed" &&
currentTask.status !== "cancelled"
) {
await extra.taskStore.updateTaskStatus(
task.taskId,
"failed",
error instanceof Error ? error.message : String(error),
);
}
} catch (statusError) {
console.error(
"[flexibleTask] Error checking/updating task status:",
statusError,
);
}
}
}
/** Creates an immediate (non-task) tool that completes after a delay. */
export function createImmediateTool(
options: ImmediateToolOptions = {},
): ToolDefinition {
const { name = "flexibleTask", delayMs = 1000 } = options;
return {
name,
description: "A tool that completes immediately without creating a task",
inputSchema: {
message: z.string().optional().describe("Optional message parameter"),
},
handler: async (
params: Record<string, unknown>,
_context?: TestServerContext,
): Promise<CallToolResult> => {
await new Promise((resolve) => setTimeout(resolve, delayMs));
return toToolResult(
`Task completed immediately: ${params.message ?? "no message"}`,
);
},
};
}
/** Creates a task tool (createTask + getTask + getTaskResult) with optional progress, elicitation, sampling, etc. */
export function createTaskTool(
options: TaskToolOptions = {},
): TaskToolDefinition {
const { name = "flexibleTask", taskSupport = "required" } = options;
return {
name,
description: `A flexible task tool supporting progress, elicitation, and sampling`,
inputSchema: {
message: z.string().optional().describe("Optional message parameter"),
},
execution: {
taskSupport: taskSupport as "required" | "optional",
},
handler: {
createTask: async (args, extra) => {
const message = (args as Record<string, unknown>)?.message as
| string
| undefined;
const progressToken = extra._meta?.progressToken;
const task = await extra.taskStore.createTask({});
runTaskExecution({
task,
extra,
message,
progressToken,
options,
}).catch(() => {});
return { task };
},
getTask: async (
_args: ShapeOutput<{ message?: z.ZodString }>,
extra: TaskRequestHandlerExtra,
): Promise<GetTaskResult> => {
const task = await extra.taskStore.getTask(extra.taskId);
return task as GetTaskResult;
},
getTaskResult: async (
_args: ShapeOutput<{ message?: z.ZodString }>,
extra: TaskRequestHandlerExtra,
): Promise<CallToolResult> => {
const result = await extra.taskStore.getTaskResult(extra.taskId);
if (!result.content) {
throw new Error("Task result does not have content field");
}
return result as CallToolResult;
},
},
};
}
/**
* Create a simple task tool that completes after a delay
*/
export function createSimpleTaskTool(
name: string = "simple_task",
delayMs: number = 1000,
): TaskToolDefinition {
return createTaskTool({ name, delayMs });
}
/**
* Create a task tool that sends progress notifications
*/
export function createProgressTaskTool(
name: string = "progress_task",
delayMs: number = 2000,
progressUnits: number = 5,
): TaskToolDefinition {
return createTaskTool({ name, delayMs, progressUnits });
}
/**
* Create a task tool that requires elicitation input
*/
export function createElicitationTaskTool(
name: string = "elicitation_task",
elicitationSchema?: z.ZodTypeAny,
): TaskToolDefinition {
return createTaskTool({
name,
elicitationSchema:
elicitationSchema ||
z.object({
input: z.string().describe("User input required for task"),
}),
});
}
/**
* Create a task tool that requires sampling input
*/
export function createSamplingTaskTool(
name: string = "sampling_task",
samplingText?: string,
): TaskToolDefinition {
return createTaskTool({
name,
samplingText: samplingText || "Please provide a response for this task",
});
}
/**
* Create a task tool with optional task support
*/
export function createOptionalTaskTool(
name: string = "optional_task",
delayMs: number = 500,
): TaskToolDefinition {
return createTaskTool({ name, taskSupport: "optional", delayMs });
}
/**
* Create a tool that does not support tasks (completes immediately without creating a task)
*/
export function createForbiddenTaskTool(
name: string = "forbidden_task",
delayMs: number = 100,
): ToolDefinition {
return createImmediateTool({ name, delayMs });
}
/**
* Create a tool that returns immediately without creating a task
* (for testing callTool() with task-supporting server config where the tool itself is immediate)
*/
export function createImmediateReturnTaskTool(
name: string = "immediate_return_task",
delayMs: number = 100,
): ToolDefinition {
return createImmediateTool({ name, delayMs });
}
/**
* Get a server config with task support and task tools for testing
*/
export function getTaskServerConfig(): ServerConfig {
return {
serverInfo: createTestServerInfo("test-task-server", "1.0.0"),
tasks: {
list: true,
cancel: true,
},
tools: [
createSimpleTaskTool(),
createProgressTaskTool(),
createElicitationTaskTool(),
createSamplingTaskTool(),
createOptionalTaskTool(),
createForbiddenTaskTool(),
createImmediateReturnTaskTool(),
],
logging: true, // Required for notifications/message and progress
};
}
/**
* Get default server config with common test tools, prompts, and resources
*/
export function getDefaultServerConfig(): ServerConfig {
return {
serverInfo: createTestServerInfo("test-mcp-server", "1.0.0"),
tools: [
createEchoTool(),
createGetSumTool(),
createGetAnnotatedMessageTool(),
createGetTempTool(),
createGetTempExtraTool(),
createSendNotificationTool(),
createWriteToStderrTool(),
createMcpAppDemoTool(),
],
prompts: [createSimplePrompt(), createArgsPrompt()],
resources: [
createArchitectureResource(),
createTestCwdResource(),
createTestEnvResource(),
createTestArgvResource(),
createMcpAppDemoResource(),
],
resourceTemplates: [
createFileResourceTemplate(),
createUserResourceTemplate(),
],
logging: true, // Required for notifications/message
};
}
/**
* OAuth Test Fixtures
*/
/**
* Creates a test server configuration with OAuth enabled
*/
export function createOAuthTestServerConfig(options: {
requireAuth?: boolean;
scopesSupported?: string[];
staticClients?: Array<{
clientId: string;
clientSecret?: string;
redirectUris?: string[];
}>;
supportDCR?: boolean;
supportCIMD?: boolean;
tokenExpirationSeconds?: number;
supportRefreshTokens?: boolean;
}): Partial<ServerConfig> {
return {
oauth: {
enabled: true,
mode: "combined",
requireAuth: options.requireAuth ?? false,
scopesSupported: options.scopesSupported ?? ["mcp"],
staticClients: options.staticClients,
supportDCR: options.supportDCR ?? false,
supportCIMD: options.supportCIMD ?? false,
tokenExpirationSeconds: options.tokenExpirationSeconds ?? 3600,
supportRefreshTokens: options.supportRefreshTokens ?? true,
},
};
}
/**
* MCP resource server that delegates authorization to an external AS (EMA / XAA testing).
* Access tokens must be JWTs from that AS (signature + iss validated via JWKS).
*/
export function createExternalResourceOAuthTestServerConfig(options: {
authorizationServers: string[];
requireAuth?: boolean;
scopesSupported?: string[];
resource?: string;
accessTokenIssuers?: string[];
jwksUri?: string;
resourceAudience?: string;
}): Partial<ServerConfig> {
return {
oauth: {
enabled: true,
mode: "protected-resource",
authorizationServers: options.authorizationServers,
requireAuth: options.requireAuth ?? true,
scopesSupported: options.scopesSupported ?? ["mcp"],
resource: options.resource,
accessTokenIssuers: options.accessTokenIssuers,
jwksUri: options.jwksUri,
resourceAudience: options.resourceAudience,
},
};
}