Skip to content

Commit 55c9f17

Browse files
committed
Implemented URL elicitation support in web app.
1 parent 6811881 commit 55c9f17

11 files changed

Lines changed: 575 additions & 48 deletions

configs/mcp.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
"test/configs/demo.json"
99
]
1010
},
11+
"url-elicitation-form": {
12+
"command": "node",
13+
"args": [
14+
"test/build/server-composable.js",
15+
"--config",
16+
"test/configs/url-elicitation-form.json"
17+
]
18+
},
1119
"everything": {
1220
"command": "npx",
1321
"args": [

docs/inspector-client-todo.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,9 @@ Goal: Parity with v1 client
8484

8585
Goal: Bring Inspector Web support to current spec
8686

87-
- URL elicitation (already in InspectorClient, just need UX)
88-
- https://github.com/modelcontextprotocol/inspector/issues/929
89-
- https://github.com/modelcontextprotocol/inspector/pull/994
90-
- May be tricky to test because everything doesn't support it yet
9187
- Add "sampling with tools" support
9288
- https://github.com/modelcontextprotocol/inspector/issues/932
93-
- Review v1 project boards for any feature deficiencies
89+
- Review v1 project boards for any feature deficiencies to spec
9490

9591
Goal: Inspector Web quality
9692

@@ -100,6 +96,22 @@ Goal: Inspector Web quality
10096
Error: waitForStateFile failed: JSON parse error (file may be mid-write or corrupt). File: /var/folders/c8/jr_qy1fs1cj3hfhr5m_2f4c40000gn/T/mcp-inspector-e2e-1771550464080-66tdoqzpfxo.json. Attempts: 40. Raw snippet: {"state":{"servers":{"http://localhost:51796/sse":{"preregisteredClientInformation":{"client_id":"test-storage-path","client_secret":"test-secret-sp"},"codeVerifier":"U8mEkBln9JtFLMzC3b50kj0QtubtwpDPU... (405 chars total). Run with DEBUG_WAIT_FOR_STATE_FILE=1 for per-attempt logs.
10197
❯ vi.waitFor.timeout.timeout test/test-helpers.ts:138:15
10298
```
99+
- Another test failure (only seen once):
100+
- **tests**/inspectorClient-oauth-e2e.test.ts > InspectorClient OAuth E2E > Resource metadata discovery and oauthStepChange ('SSE') > should discover resource metadata and set resource in guided flow
101+
```
102+
Error: Failed to discover OAuth metadata
103+
❯ Object.execute auth/state-machine.ts:72:15
104+
70| );
105+
71| if (!metadata) {
106+
72| throw new Error("Failed to discover OAuth metadata");
107+
| ^
108+
73| }
109+
74| const parsedMetadata = await OAuthMetadataSchema.parseAsync(metadata);
110+
❯ OAuthStateMachine.executeStep auth/state-machine.ts:283:5
111+
❯ InspectorClient.beginGuidedAuth mcp/inspectorClient.ts:3170:5
112+
❯ InspectorClient.runGuidedAuth mcp/inspectorClient.ts:3184:7
113+
__tests__/inspectorClient-oauth-e2e.test.ts:1369:9
114+
```
103115
- Review open v1 bugs (esp auth bugs) to see which ones still apply
104116
105117
Misc
@@ -123,3 +135,12 @@ Better forms (test tool, etc)
123135
124136
- UX (cleaner, maybe ditch ink-forms, see if it can be styled better?)
125137
- Functionality (data types, arrays, arrays of objects, etc)
138+
139+
## URL Elicitation
140+
141+
- https://github.com/modelcontextprotocol/inspector/issues/929
142+
- https://github.com/modelcontextprotocol/inspector/pull/994
143+
144+
Not supported in everything server yet, so we tested via composable server:
145+
146+
`npm run web:dev -- --config configs/mcp.json --server url-elicitation-form`

docs/mcp-feature-tracker.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Track MCP feature support across InspectorClient, Web v1, Web v1.5, and TUI.
3939
| Sampling requests |||||
4040
| Sampling with tools |||||
4141
| Elicitation requests (form) |||||
42-
| Elicitation requests (url) ||| ||
42+
| Elicitation requests (url) ||| ||
4343
| Tasks (long-running operations) |||||
4444
| Requestor task support |||||
4545
| Completions (resource templates) |||||

test/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ The **@modelcontextprotocol/server-everything** package is the standard test-ben
1313

1414
The composable test server is **complementary**, not a replacement:
1515

16-
| Situation | Composable server advantage | Everything server |
17-
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
18-
| **Testing specific capability combinations** | Compose exactly the subset you need (e.g. tools only, resources only, tasks + resources). | Fixed "kitchen sink" shape. |
19-
| **Pagination testing** | `maxPageSize` configurable per list type. Test cursor behavior with small page sizes (e.g. 2 or 3). | Fixed resource/prompt counts. No configurable pagination. |
20-
| **Controlled, predictable behavior** | No random log messages or timers. Responses are deterministic. | Random log messages every 15 seconds, subscription updates every 5 seconds. |
21-
| **listChanged / subscriptions** | Enable or disable `listChanged` per list type. `subscriptions` toggle for resource updates. | Fixed behavior. |
22-
| **Task variants** | Test task tools in isolation: immediate, progress, elicitation, sampling, optional vs required. | Has task-like behavior in a fixed form. |
23-
| **Rapid iteration** | Swap config files to test different server shapes without code changes. | Single fixed shape. |
16+
| Situation | Composable server advantage | Everything server |
17+
| -------------------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
18+
| **Testing specific capability combinations** | Compose exactly the subset you need (e.g. tools only, resources only, tasks + resources). | Fixed "kitchen sink" shape. |
19+
| **Pagination testing** | `maxPageSize` configurable per list type. Test cursor behavior with small page sizes (e.g. 2 or 3). | Fixed resource/prompt counts. No configurable pagination. |
20+
| **Controlled, predictable behavior** | No unexpected notifications, log evenets, etc. Behavior is focussed and deterministic. | Various log messages, notifications, subscription updates, etc. |
21+
| **listChanged / subscriptions** | Enable or disable `listChanged` per list type. `subscriptions` toggle for resource updates. | Fixed behavior. |
22+
| **Task variants** | Test task tools in isolation: immediate, progress, elicitation, sampling, optional vs required. | Has task-like behavior in a fixed form. |
23+
| **Rapid iteration** | Swap config files to test different server shapes without code changes. | Single fixed shape. |
2424

2525
**Use Everything when:** You want broad coverage, community standard, quick `npx` start, or hosted DCR-only OAuth for testing against a real auth server.
2626

web/src/App.tsx

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ const App = () => {
467467
sessionId,
468468
receiverTasks: true,
469469
receiverTaskTtlMs: getMCPTaskTtl(currentConfig),
470+
elicit: { form: true, url: true },
470471
};
471472

472473
if (hasOAuthConfig) {
@@ -597,32 +598,64 @@ const App = () => {
597598
const elicitation = event.detail;
598599
const currentTab = lastToolCallOriginTabRef.current;
599600
const numericId = getNumericId(elicitation.id);
601+
const params = elicitation.request.params ?? {};
602+
const isUrl = params.mode === "url";
603+
604+
const baseItem = {
605+
id: numericId,
606+
elicitationId: elicitation.id as string,
607+
originatingTab: currentTab,
608+
resolve: async (result: ElicitationResponse) => {
609+
await elicitation.respond(result);
610+
},
611+
decline: async (error: Error) => {
612+
elicitation.reject(error);
613+
elicitation.remove();
614+
console.error("Elicitation request rejected:", error);
615+
},
616+
};
600617

601-
setPendingElicitationRequests((prev) => [
602-
...prev,
603-
{
604-
id: numericId,
605-
request: {
606-
id: numericId,
607-
message: elicitation.request.params.message,
608-
requestedSchema: elicitation.request.params.requestedSchema,
609-
},
610-
originatingTab: currentTab,
611-
resolve: async (result: ElicitationResponse) => {
612-
await elicitation.respond(result);
618+
if (isUrl) {
619+
setPendingElicitationRequests((prev) => [
620+
...prev,
621+
{
622+
...baseItem,
623+
request: {
624+
mode: "url",
625+
id: numericId,
626+
message: params.message as string,
627+
url: params.url as string,
628+
elicitationId: params.elicitationId as string,
629+
},
613630
},
614-
decline: async (error: Error) => {
615-
elicitation.reject(error);
616-
elicitation.remove();
617-
console.error("Elicitation request rejected:", error);
631+
]);
632+
} else {
633+
setPendingElicitationRequests((prev) => [
634+
...prev,
635+
{
636+
...baseItem,
637+
request: {
638+
mode: "form",
639+
id: numericId,
640+
message: params.message as string,
641+
requestedSchema: params.requestedSchema,
642+
},
618643
},
619-
},
620-
]);
644+
]);
645+
}
621646

622647
setActiveTab("elicitations");
623648
window.location.hash = "elicitations";
624649
};
625650

651+
const handlePendingElicitationsChange = () => {
652+
const stillPending = inspectorClient.getPendingElicitations();
653+
const pendingIds = new Set(stillPending.map((e) => e.id));
654+
setPendingElicitationRequests((prev) =>
655+
prev.filter((r) => pendingIds.has(r.elicitationId)),
656+
);
657+
};
658+
626659
inspectorClient.addEventListener(
627660
"newPendingSample",
628661
handleNewPendingSample,
@@ -631,6 +664,10 @@ const App = () => {
631664
"newPendingElicitation",
632665
handleNewPendingElicitation,
633666
);
667+
inspectorClient.addEventListener(
668+
"pendingElicitationsChange",
669+
handlePendingElicitationsChange,
670+
);
634671

635672
return () => {
636673
inspectorClient.removeEventListener(
@@ -641,6 +678,10 @@ const App = () => {
641678
"newPendingElicitation",
642679
handleNewPendingElicitation,
643680
);
681+
inspectorClient.removeEventListener(
682+
"pendingElicitationsChange",
683+
handlePendingElicitationsChange,
684+
);
644685
};
645686
}, [inspectorClient]);
646687

web/src/components/ElicitationRequest.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import JsonView from "./JsonView";
55
import { JsonSchemaType, JsonValue } from "@/utils/jsonUtils";
66
import { generateDefaultValue } from "@/utils/schemaUtils";
77
import {
8-
PendingElicitationRequest,
8+
PendingFormElicitationRequest,
99
ElicitationResponse,
1010
} from "./ElicitationTab";
1111
import Ajv from "ajv";
1212

1313
export type ElicitationRequestProps = {
14-
request: PendingElicitationRequest;
14+
request: PendingFormElicitationRequest;
1515
onResolve: (id: number, response: ElicitationResponse) => void;
1616
};
1717

web/src/components/ElicitationTab.tsx

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,52 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
22
import { TabsContent } from "@/components/ui/tabs";
33
import { JsonSchemaType } from "@/utils/jsonUtils";
44
import ElicitationRequest from "./ElicitationRequest";
5+
import ElicitationUrlRequest from "./ElicitationUrlRequest";
56

6-
export interface ElicitationRequestData {
7+
/** Form-mode elicitation request payload */
8+
export interface FormElicitationRequestData {
9+
mode: "form";
710
id: number;
811
message: string;
912
requestedSchema: JsonSchemaType;
1013
}
1114

15+
/** URL-mode elicitation request payload */
16+
export interface UrlElicitationRequestData {
17+
mode: "url";
18+
id: number;
19+
message: string;
20+
url: string;
21+
elicitationId: string;
22+
}
23+
24+
export type ElicitationRequestData =
25+
| FormElicitationRequestData
26+
| UrlElicitationRequestData;
27+
1228
export interface ElicitationResponse {
1329
action: "accept" | "decline" | "cancel";
1430
content?: Record<string, unknown>;
1531
}
1632

1733
export type PendingElicitationRequest = {
1834
id: number;
35+
/** Client-side id (ElicitationCreateMessage.id) for syncing with getPendingElicitations() */
36+
elicitationId: string;
1937
request: ElicitationRequestData;
2038
originatingTab?: string;
2139
};
2240

41+
/** Pending form-only request; use for ElicitationRequest component */
42+
export type PendingFormElicitationRequest = PendingElicitationRequest & {
43+
request: FormElicitationRequestData;
44+
};
45+
46+
/** Pending URL-only request; use for ElicitationUrlRequest component */
47+
export type PendingUrlElicitationRequest = PendingElicitationRequest & {
48+
request: UrlElicitationRequestData;
49+
};
50+
2351
export type Props = {
2452
pendingRequests: PendingElicitationRequest[];
2553
onResolve: (id: number, response: ElicitationResponse) => void;
@@ -37,13 +65,21 @@ const ElicitationTab = ({ pendingRequests, onResolve }: Props) => {
3765
</Alert>
3866
<div className="mt-4 space-y-4">
3967
<h3 className="text-lg font-semibold">Recent Requests</h3>
40-
{pendingRequests.map((request) => (
41-
<ElicitationRequest
42-
key={request.id}
43-
request={request}
44-
onResolve={onResolve}
45-
/>
46-
))}
68+
{pendingRequests.map((request) =>
69+
request.request.mode === "url" ? (
70+
<ElicitationUrlRequest
71+
key={request.id}
72+
request={request as PendingUrlElicitationRequest}
73+
onResolve={onResolve}
74+
/>
75+
) : (
76+
<ElicitationRequest
77+
key={request.id}
78+
request={request as PendingFormElicitationRequest}
79+
onResolve={onResolve}
80+
/>
81+
),
82+
)}
4783
{pendingRequests.length === 0 && (
4884
<p className="text-gray-500">No pending requests</p>
4985
)}

0 commit comments

Comments
 (0)