Skip to content

Commit 53b6efd

Browse files
krubenokCopilot
andcommitted
fix(server): require hierarchical elicitation UI URIs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 89ab2bc commit 53b6efd

2 files changed

Lines changed: 50 additions & 24 deletions

File tree

src/server/index.test.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -414,14 +414,11 @@ describe("elicitation UI metadata", () => {
414414
},
415415
};
416416

417-
it("attaches and reads an absolute ui:// resource URI", () => {
418-
const enhanced = withElicitationUi(
419-
params,
420-
"ui://example/choose-option.html",
421-
);
417+
it("attaches and reads an absolute hierarchical ui:// resource URI", () => {
418+
const enhanced = withElicitationUi(params, "ui://weather/view.html");
422419
expect(enhanced.requestedSchema).toEqual(params.requestedSchema);
423420
expect(getElicitationUiResourceUri(enhanced)).toBe(
424-
"ui://example/choose-option.html",
421+
"ui://weather/view.html",
425422
);
426423
});
427424

@@ -441,4 +438,29 @@ describe("elicitation UI metadata", () => {
441438
"absolute ui://",
442439
);
443440
});
441+
442+
it.each([
443+
"ui:example/view.html",
444+
"ui:///view.html",
445+
"ui://",
446+
"ui://[invalid",
447+
])("rejects invalid resource URI in the setter: %s", (resourceUri) => {
448+
expect(() => withElicitationUi(params, resourceUri)).toThrow(
449+
"absolute ui://",
450+
);
451+
});
452+
453+
it.each([
454+
"ui:example/view.html",
455+
"ui:///view.html",
456+
"ui://",
457+
"ui://[invalid",
458+
])("rejects invalid resource URI in the getter: %s", (resourceUri) => {
459+
expect(() =>
460+
getElicitationUiResourceUri({
461+
...params,
462+
_meta: { ui: { resourceUri } },
463+
}),
464+
).toThrow("absolute ui://");
465+
});
444466
});

src/server/index.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,26 @@ export function supportsAppElicitation(
522522
);
523523
}
524524

525+
const ELICITATION_UI_URI_ERROR =
526+
"Elicitation UI resourceUri must be an absolute ui:// URI";
527+
528+
function validateElicitationUiResourceUri(resourceUri: string): void {
529+
let parsed: URL;
530+
try {
531+
parsed = new URL(resourceUri);
532+
} catch {
533+
throw new Error(ELICITATION_UI_URI_ERROR);
534+
}
535+
536+
if (
537+
parsed.protocol !== "ui:" ||
538+
!/^ui:\/\//i.test(resourceUri) ||
539+
parsed.host.length === 0
540+
) {
541+
throw new Error(ELICITATION_UI_URI_ERROR);
542+
}
543+
}
544+
525545
/**
526546
* Attach an MCP App resource to a standard form-mode elicitation request.
527547
*/
@@ -535,15 +555,7 @@ export function withElicitationUi(
535555
throw new Error("MCP Apps only support form-mode elicitations");
536556
}
537557

538-
let parsed: URL;
539-
try {
540-
parsed = new URL(resourceUri);
541-
} catch {
542-
throw new Error("Elicitation UI resourceUri must be an absolute ui:// URI");
543-
}
544-
if (parsed.protocol !== "ui:") {
545-
throw new Error("Elicitation UI resourceUri must be an absolute ui:// URI");
546-
}
558+
validateElicitationUiResourceUri(resourceUri);
547559

548560
const meta = params._meta ?? {};
549561
const ui =
@@ -578,14 +590,6 @@ export function getElicitationUiResourceUri(
578590
throw new Error("Elicitation UI resourceUri must be a string");
579591
}
580592

581-
let parsed: URL;
582-
try {
583-
parsed = new URL(resourceUri);
584-
} catch {
585-
throw new Error("Elicitation UI resourceUri must be an absolute ui:// URI");
586-
}
587-
if (parsed.protocol !== "ui:") {
588-
throw new Error("Elicitation UI resourceUri must be an absolute ui:// URI");
589-
}
593+
validateElicitationUiResourceUri(resourceUri);
590594
return resourceUri;
591595
}

0 commit comments

Comments
 (0)