Skip to content

Commit 28bd5a0

Browse files
committed
url elicitation normal/error path
1 parent c9c41ae commit 28bd5a0

File tree

6 files changed

+163
-2
lines changed

6 files changed

+163
-2
lines changed

src/everything/__tests__/registrations.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,16 @@ describe('Registration Index Files', () => {
6767

6868
registerConditionalTools(mockServerWithCapabilities);
6969

70-
// Should register 4 conditional tools + 3 task-based tools when all capabilities present
71-
expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(4);
70+
// Should register 5 conditional tools + 3 task-based tools when all capabilities present
71+
expect(mockServerWithCapabilities.registerTool).toHaveBeenCalledTimes(5);
7272

7373
const registeredTools = (
7474
mockServerWithCapabilities.registerTool as any
7575
).mock.calls.map((call: any[]) => call[0]);
7676
expect(registeredTools).toContain('get-roots-list');
7777
expect(registeredTools).toContain('trigger-elicitation-request');
7878
expect(registeredTools).toContain('trigger-url-elicitation-request');
79+
expect(registeredTools).toContain('trigger-url-elicitation-required-error');
7980
expect(registeredTools).toContain('trigger-sampling-request');
8081

8182
// Task-based tools are registered via experimental.tasks.registerToolTask

src/everything/__tests__/tools.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { registerToggleSubscriberUpdatesTool } from '../tools/toggle-subscriber-
1414
import { registerTriggerSamplingRequestTool } from '../tools/trigger-sampling-request.js';
1515
import { registerTriggerElicitationRequestTool } from '../tools/trigger-elicitation-request.js';
1616
import { registerTriggerUrlElicitationRequestTool } from '../tools/trigger-url-elicitation-request.js';
17+
import { registerTriggerUrlElicitationRequiredErrorTool } from '../tools/trigger-url-elicitation-required-error.js';
1718
import { registerGetRootsListTool } from '../tools/get-roots-list.js';
1819
import { registerGZipFileAsResourceTool } from '../tools/gzip-file-as-resource.js';
1920

@@ -802,6 +803,80 @@ describe('Tools', () => {
802803
});
803804
});
804805

806+
describe('trigger-url-elicitation-required-error', () => {
807+
it('should not register when client does not support URL elicitation', () => {
808+
const handlers: Map<string, Function> = new Map();
809+
const mockServer = {
810+
registerTool: vi.fn((name: string, config: any, handler: Function) => {
811+
handlers.set(name, handler);
812+
}),
813+
server: {
814+
getClientCapabilities: vi.fn(() => ({ elicitation: { form: {} } })),
815+
},
816+
} as unknown as McpServer;
817+
818+
registerTriggerUrlElicitationRequiredErrorTool(mockServer);
819+
820+
expect(mockServer.registerTool).not.toHaveBeenCalled();
821+
});
822+
823+
it('should register when client supports URL elicitation', () => {
824+
const handlers: Map<string, Function> = new Map();
825+
const mockServer = {
826+
registerTool: vi.fn((name: string, config: any, handler: Function) => {
827+
handlers.set(name, handler);
828+
}),
829+
server: {
830+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
831+
},
832+
} as unknown as McpServer;
833+
834+
registerTriggerUrlElicitationRequiredErrorTool(mockServer);
835+
836+
expect(mockServer.registerTool).toHaveBeenCalledWith(
837+
'trigger-url-elicitation-required-error',
838+
expect.objectContaining({
839+
title: 'Trigger URL Elicitation Required Error Tool',
840+
}),
841+
expect.any(Function)
842+
);
843+
});
844+
845+
it('should throw MCP error -32042 with required URL elicitation data', async () => {
846+
const handlers: Map<string, Function> = new Map();
847+
const mockServer = {
848+
registerTool: vi.fn((name: string, config: any, handler: Function) => {
849+
handlers.set(name, handler);
850+
}),
851+
server: {
852+
getClientCapabilities: vi.fn(() => ({ elicitation: { url: {} } })),
853+
},
854+
} as unknown as McpServer;
855+
856+
registerTriggerUrlElicitationRequiredErrorTool(mockServer);
857+
858+
const handler = handlers.get('trigger-url-elicitation-required-error')!;
859+
860+
expect.assertions(2);
861+
862+
try {
863+
await handler({
864+
url: 'https://example.com/connect',
865+
message: 'Authorization is required to continue.',
866+
elicitationId: 'elicitation-xyz',
867+
});
868+
} catch (error: any) {
869+
expect(error.code).toBe(-32042);
870+
expect(error.data.elicitations[0]).toEqual({
871+
mode: 'url',
872+
url: 'https://example.com/connect',
873+
message: 'Authorization is required to continue.',
874+
elicitationId: 'elicitation-xyz',
875+
});
876+
}
877+
});
878+
});
879+
805880
describe('get-roots-list', () => {
806881
it('should not register when client does not support roots', () => {
807882
const { mockServer } = createMockServer();

src/everything/docs/features.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- `toggle-subscriber-updates` (tools/toggle-subscriber-updates.ts): Starts or stops simulated resource update notifications for URIs the invoking session has subscribed to.
2525
- `trigger-elicitation-request` (tools/trigger-elicitation-request.ts): Issues an `elicitation/create` request using form-mode fields (strings, numbers, booleans, enums, and format validation) and returns the resulting action/content.
2626
- `trigger-url-elicitation-request` (tools/trigger-url-elicitation-request.ts): Issues an `elicitation/create` request in URL mode (`mode: "url"`) with an `elicitationId`, and can optionally emit `notifications/elicitation/complete` after acceptance. Requires client capability `elicitation.url`.
27+
- `trigger-url-elicitation-required-error` (tools/trigger-url-elicitation-required-error.ts): Throws MCP error `-32042` (`UrlElicitationRequiredError`) with one or more required URL-mode elicitations in `error.data.elicitations`, demonstrating the retry-after-elicitation flow.
2728
- `trigger-sampling-request` (tools/trigger-sampling-request.ts): Issues a `sampling/createMessage` request to the client/LLM using provided `prompt` and optional generation controls; returns the LLM's response payload.
2829
- `simulate-research-query` (tools/simulate-research-query.ts): Demonstrates MCP Tasks (SEP-1686) with a simulated multi-stage research operation. Accepts `topic` and `ambiguous` parameters. Returns a task that progresses through stages with status updates. If `ambiguous` is true and client supports elicitation, sends an elicitation request directly to gather clarification before completing.
2930
- `trigger-sampling-request-async` (tools/trigger-sampling-request-async.ts): Demonstrates bidirectional tasks where the server sends a sampling request that the client executes as a background task. Server polls for status and retrieves the LLM result when complete. Requires client to support `tasks.requests.sampling.createMessage`.

src/everything/docs/structure.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ src/everything
5252
│ ├── toggle-subscriber-updates.ts
5353
│ ├── trigger-elicitation-request.ts
5454
│ ├── trigger-long-running-operation.ts
55+
│ ├── trigger-url-elicitation-required-error.ts
5556
│ ├── trigger-url-elicitation-request.ts
5657
│ └── trigger-sampling-request.ts
5758
└── transports
@@ -152,6 +153,8 @@ src/everything
152153
- Registers a `trigger-elicitation-request` tool that sends an `elicitation/create` request to the client/LLM and returns the elicitation result.
153154
- `trigger-url-elicitation-request.ts`
154155
- Registers a `trigger-url-elicitation-request` tool that sends an out-of-band URL-mode `elicitation/create` request (`mode: "url"`) including an `elicitationId`, and can optionally emit `notifications/elicitation/complete` after acceptance.
156+
- `trigger-url-elicitation-required-error.ts`
157+
- Registers a `trigger-url-elicitation-required-error` tool that throws MCP error `-32042` (`UrlElicitationRequiredError`) with required URL-mode elicitation params in `error.data.elicitations`.
155158
- `trigger-sampling-request.ts`
156159
- Registers a `trigger-sampling-request` tool that sends a `sampling/createMessage` request to the client/LLM and returns the sampling result.
157160
- `get-structured-content.ts`

src/everything/tools/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { registerTriggerSamplingRequestAsyncTool } from "./trigger-sampling-requ
1818
import { registerTriggerElicitationRequestAsyncTool } from "./trigger-elicitation-request-async.js";
1919
import { registerSimulateResearchQueryTool } from "./simulate-research-query.js";
2020
import { registerTriggerUrlElicitationRequestTool } from "./trigger-url-elicitation-request.js";
21+
import { registerTriggerUrlElicitationRequiredErrorTool } from "./trigger-url-elicitation-required-error.js";
2122

2223
/**
2324
* Register the tools with the MCP server.
@@ -46,6 +47,7 @@ export const registerConditionalTools = (server: McpServer) => {
4647
registerGetRootsListTool(server);
4748
registerTriggerElicitationRequestTool(server);
4849
registerTriggerUrlElicitationRequestTool(server);
50+
registerTriggerUrlElicitationRequiredErrorTool(server);
4951
registerTriggerSamplingRequestTool(server);
5052
// Task-based research tool (uses experimental tasks API)
5153
registerSimulateResearchQueryTool(server);
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { randomUUID } from "node:crypto";
2+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3+
import {
4+
CallToolResult,
5+
ElicitRequestURLParams,
6+
UrlElicitationRequiredError,
7+
} from "@modelcontextprotocol/sdk/types.js";
8+
import { z } from "zod";
9+
10+
// Tool input schema
11+
const TriggerUrlElicitationRequiredErrorSchema = z.object({
12+
url: z.string().url().describe("The URL the user should open"),
13+
message: z
14+
.string()
15+
.default("This request requires more information.")
16+
.describe("Message shown to the user for the URL elicitation"),
17+
elicitationId: z
18+
.string()
19+
.optional()
20+
.describe("Optional explicit elicitation ID. Defaults to a random UUID."),
21+
});
22+
23+
// Tool configuration
24+
const name = "trigger-url-elicitation-required-error";
25+
const config = {
26+
title: "Trigger URL Elicitation Required Error Tool",
27+
description:
28+
"Returns MCP error -32042 (URL elicitation required) so clients can handle URL-mode elicitations via the error path.",
29+
inputSchema: TriggerUrlElicitationRequiredErrorSchema,
30+
};
31+
32+
/**
33+
* Registers the 'trigger-url-elicitation-required-error' tool.
34+
*
35+
* This tool demonstrates the MCP error path for URL elicitation by throwing
36+
* UrlElicitationRequiredError (code -32042) from a tool handler.
37+
*
38+
* @param {McpServer} server - The McpServer instance where the tool will be registered.
39+
*/
40+
export const registerTriggerUrlElicitationRequiredErrorTool = (
41+
server: McpServer
42+
) => {
43+
const clientCapabilities = server.server.getClientCapabilities() || {};
44+
const clientElicitationCapabilities = clientCapabilities.elicitation as
45+
| {
46+
url?: object;
47+
}
48+
| undefined;
49+
50+
const clientSupportsUrlElicitation =
51+
clientElicitationCapabilities?.url !== undefined;
52+
53+
if (clientSupportsUrlElicitation) {
54+
server.registerTool(
55+
name,
56+
config,
57+
async (args): Promise<CallToolResult> => {
58+
const validatedArgs = TriggerUrlElicitationRequiredErrorSchema.parse(args);
59+
const { url, message, elicitationId: requestedElicitationId } =
60+
validatedArgs;
61+
62+
const elicitationId = requestedElicitationId ?? randomUUID();
63+
64+
const requiredElicitation: ElicitRequestURLParams = {
65+
mode: "url",
66+
url,
67+
message,
68+
elicitationId,
69+
};
70+
71+
throw new UrlElicitationRequiredError(
72+
[requiredElicitation],
73+
"This request requires more information."
74+
);
75+
}
76+
);
77+
}
78+
};
79+

0 commit comments

Comments
 (0)