Addresses Copilot's review on #2035. All three findings reproduced against the pinned SDK before acting. 1. `#` was misclassified as required. Measured: `x://a{#frag}` with no `frag` expands to exactly `x://a`, a well-formed URI naming a real resource -- unlike `{+path}` (`x://a/`) or a simple `{userId}` (`file:///users//profile`), which leave an empty path segment. Moved `#` into the omittable set; the required cases now assert what the URI *becomes* when blank, so the rule is checked rather than asserted. 2. Delegating to the SDK did not actually give RFC 6570 expansion. `UriTemplate.expandPart` takes an early `names.length > 1` branch that raw-joins values, skipping both `encodeValue` and the operator prefix: `x://{a,b}` with `a = "foo/bar"` expands to `x://foo/bar,q` -- the very unencoded-slash defect this PR is about -- and `x://a{/p,q}` to `x://ax y,z`. Only `?`/`&` are correct, being dispatched earlier. Fixing that in the web client alone would have left the TUI and CLI wrong, since `readResourceFromTemplate` expands through the same class. So parse/classify/expand now live in `core/mcp/uriTemplate.ts` and both call sites use it. The correction is surgical: a multi-name non-query expression is expanded here and spliced in as literal text before the SDK sees the template (safe -- both encoders escape `{`/`}`), while every single-name and query expression still goes through the SDK untouched. The preview applies the same correction, so it cannot promise a URI that submitting would not send. 3. The showcase promised a blank `{?topic}` read that did not work. `UriTemplate.match()` compiles `{?topic}` to a *required* `\?topic=([^&]+)`, so `match("foobar://events")` returns null and the read 404s. A real server exposes the unfiltered collection as its own resource; the showcase now registers `foobar://events` so the documented step resolves. Verified end to end through the CLI. Signed-off-by: cliffhall <cliff@futurescale.com>
312 lines
9.1 KiB
TypeScript
312 lines
9.1 KiB
TypeScript
/**
|
|
* Preset registry for config-driven composable server
|
|
* Maps preset names to fixture factory functions
|
|
*/
|
|
|
|
import type {
|
|
ToolDefinition,
|
|
TaskToolDefinition,
|
|
ResourceDefinition,
|
|
PromptDefinition,
|
|
ResourceTemplateDefinition,
|
|
} from "./composable-test-server.js";
|
|
import {
|
|
createEchoTool,
|
|
createNullableFieldsTool,
|
|
createGetEnvTool,
|
|
createAddTool,
|
|
createGetSumTool,
|
|
createGetWeatherTool,
|
|
createInvalidHeaderTool,
|
|
createSpecErrorTriggerTool,
|
|
createWriteToStderrTool,
|
|
createCollectSampleTool,
|
|
createListRootsTool,
|
|
createCollectFormElicitationTool,
|
|
createMrtrTool,
|
|
createMrtrMultiRoundTool,
|
|
createMrtrRootsTool,
|
|
createMrtrSamplingTool,
|
|
createMrtrLoopTool,
|
|
createMrtrEdgeCaseTool,
|
|
createCollectUrlElicitationTool,
|
|
createUrlElicitationFormTool,
|
|
createSendNotificationTool,
|
|
createGetAnnotatedMessageTool,
|
|
createGetTempTool,
|
|
createGetTempExtraTool,
|
|
createListItemsTool,
|
|
createAddResourceTool,
|
|
createRemoveResourceTool,
|
|
createAddToolTool,
|
|
createRemoveToolTool,
|
|
createAddPromptTool,
|
|
createRemovePromptTool,
|
|
createUpdateResourceTool,
|
|
createSendProgressTool,
|
|
createNumberedTools,
|
|
createSimpleTaskTool,
|
|
createProgressTaskTool,
|
|
createElicitationTaskTool,
|
|
createSamplingTaskTool,
|
|
createOptionalTaskTool,
|
|
createForbiddenTaskTool,
|
|
createImmediateReturnTaskTool,
|
|
createMcpAppDemoTool,
|
|
createMcpAppDemoResource,
|
|
createArchitectureResource,
|
|
createTestCwdResource,
|
|
createTestEnvResource,
|
|
createTestArgvResource,
|
|
createNumberedResources,
|
|
createFileResourceTemplate,
|
|
createUserResourceTemplate,
|
|
createNumberedResourceTemplates,
|
|
createRfc6570ResourceTemplates,
|
|
createRfc6570BaseResource,
|
|
createSimplePrompt,
|
|
createArgsPrompt,
|
|
createNumberedPrompts,
|
|
} from "./test-server-fixtures.js";
|
|
|
|
export type PresetType = "tool" | "resource" | "resourceTemplate" | "prompt";
|
|
|
|
export type PresetResult =
|
|
| ToolDefinition
|
|
| TaskToolDefinition
|
|
| ResourceDefinition
|
|
| PromptDefinition
|
|
| ResourceTemplateDefinition
|
|
| (ToolDefinition | TaskToolDefinition)[]
|
|
| ResourceDefinition[]
|
|
| ResourceTemplateDefinition[]
|
|
| PromptDefinition[];
|
|
|
|
function resolveToolPreset(
|
|
name: string,
|
|
params?: Record<string, unknown>,
|
|
):
|
|
| ToolDefinition
|
|
| TaskToolDefinition
|
|
| (ToolDefinition | TaskToolDefinition)[] {
|
|
const p = params ?? {};
|
|
const get = (k: string) => p[k] as unknown;
|
|
switch (name) {
|
|
case "echo":
|
|
return createEchoTool();
|
|
case "record_shipment":
|
|
case "nullable_fields":
|
|
return createNullableFieldsTool();
|
|
case "get-env":
|
|
case "get_env":
|
|
return createGetEnvTool();
|
|
case "add":
|
|
return createAddTool();
|
|
case "get_sum":
|
|
return createGetSumTool();
|
|
case "get_weather":
|
|
return createGetWeatherTool();
|
|
case "invalid_header_tool":
|
|
return createInvalidHeaderTool();
|
|
case "trigger_header_mismatch":
|
|
return createSpecErrorTriggerTool(
|
|
"trigger_header_mismatch",
|
|
"Returns -32020 HeaderMismatch (HTTP 400)",
|
|
);
|
|
case "trigger_missing_capability":
|
|
return createSpecErrorTriggerTool(
|
|
"trigger_missing_capability",
|
|
"Returns -32021 MissingRequiredClientCapability (HTTP 400)",
|
|
);
|
|
case "trigger_unsupported_version":
|
|
return createSpecErrorTriggerTool(
|
|
"trigger_unsupported_version",
|
|
"Returns -32022 UnsupportedProtocolVersion (HTTP 400)",
|
|
);
|
|
case "trigger_method_not_found":
|
|
return createSpecErrorTriggerTool(
|
|
"trigger_method_not_found",
|
|
"Returns -32601 MethodNotFound (HTTP 404)",
|
|
);
|
|
case "trigger_invalid_params":
|
|
return createSpecErrorTriggerTool(
|
|
"trigger_invalid_params",
|
|
"Returns a generic -32602 Invalid params (not an unknown tool)",
|
|
);
|
|
case "write_to_stderr":
|
|
return createWriteToStderrTool();
|
|
case "collect_sample":
|
|
return createCollectSampleTool();
|
|
case "list_roots":
|
|
return createListRootsTool();
|
|
case "collect_elicitation":
|
|
return createCollectFormElicitationTool();
|
|
case "mrtr_confirm":
|
|
return createMrtrTool();
|
|
case "mrtr_two_step":
|
|
return createMrtrMultiRoundTool();
|
|
case "mrtr_roots":
|
|
return createMrtrRootsTool();
|
|
case "mrtr_sample":
|
|
return createMrtrSamplingTool();
|
|
case "mrtr_loop":
|
|
return createMrtrLoopTool();
|
|
case "mrtr_edge":
|
|
return createMrtrEdgeCaseTool();
|
|
case "collect_url_elicitation":
|
|
return createCollectUrlElicitationTool();
|
|
case "url_elicitation_form":
|
|
return createUrlElicitationFormTool();
|
|
case "send_notification":
|
|
return createSendNotificationTool();
|
|
case "get_annotated_message":
|
|
return createGetAnnotatedMessageTool();
|
|
case "get_temp":
|
|
return createGetTempTool();
|
|
case "get_temp_extra":
|
|
return createGetTempExtraTool();
|
|
case "list_items":
|
|
return createListItemsTool();
|
|
case "add_resource":
|
|
return createAddResourceTool();
|
|
case "remove_resource":
|
|
return createRemoveResourceTool();
|
|
case "add_tool":
|
|
return createAddToolTool();
|
|
case "remove_tool":
|
|
return createRemoveToolTool();
|
|
case "add_prompt":
|
|
return createAddPromptTool();
|
|
case "remove_prompt":
|
|
return createRemovePromptTool();
|
|
case "update_resource":
|
|
return createUpdateResourceTool();
|
|
case "send_progress":
|
|
return createSendProgressTool(get("name") as string | undefined);
|
|
case "numbered_tools":
|
|
return createNumberedTools(Number(get("count")) || 5);
|
|
case "simple_task":
|
|
return createSimpleTaskTool(
|
|
get("name") as string | undefined,
|
|
Number(get("delayMs")) || undefined,
|
|
);
|
|
case "progress_task":
|
|
return createProgressTaskTool(
|
|
get("name") as string | undefined,
|
|
Number(get("delayMs")) || undefined,
|
|
Number(get("progressUnits")) || undefined,
|
|
);
|
|
case "elicitation_task":
|
|
return createElicitationTaskTool(get("name") as string | undefined);
|
|
case "sampling_task":
|
|
return createSamplingTaskTool(
|
|
get("name") as string | undefined,
|
|
get("samplingText") as string | undefined,
|
|
);
|
|
case "optional_task":
|
|
return createOptionalTaskTool(
|
|
get("name") as string | undefined,
|
|
Number(get("delayMs")) || undefined,
|
|
);
|
|
case "forbidden_task":
|
|
return createForbiddenTaskTool(
|
|
get("name") as string | undefined,
|
|
Number(get("delayMs")) || undefined,
|
|
);
|
|
case "immediate_return_task":
|
|
return createImmediateReturnTaskTool(
|
|
get("name") as string | undefined,
|
|
Number(get("delayMs")) || undefined,
|
|
);
|
|
case "mcp_app_demo":
|
|
return createMcpAppDemoTool();
|
|
default:
|
|
throw new Error(`Unknown tool preset: ${name}`);
|
|
}
|
|
}
|
|
|
|
function resolveResourcePreset(
|
|
name: string,
|
|
params?: Record<string, unknown>,
|
|
): ResourceDefinition | ResourceDefinition[] {
|
|
const p = params ?? {};
|
|
const get = (k: string) => p[k] as unknown;
|
|
switch (name) {
|
|
case "architecture":
|
|
return createArchitectureResource();
|
|
case "test_cwd":
|
|
return createTestCwdResource();
|
|
case "test_env":
|
|
return createTestEnvResource();
|
|
case "test_argv":
|
|
return createTestArgvResource();
|
|
case "numbered_resources":
|
|
return createNumberedResources(Number(get("count")) || 3);
|
|
case "mcp_app_demo_widget":
|
|
return createMcpAppDemoResource();
|
|
case "rfc6570_base":
|
|
return createRfc6570BaseResource();
|
|
default:
|
|
throw new Error(`Unknown resource preset: ${name}`);
|
|
}
|
|
}
|
|
|
|
function resolveResourceTemplatePreset(
|
|
name: string,
|
|
params?: Record<string, unknown>,
|
|
): ResourceTemplateDefinition | ResourceTemplateDefinition[] {
|
|
const p = params ?? {};
|
|
const get = (k: string) => p[k] as unknown;
|
|
switch (name) {
|
|
case "file":
|
|
return createFileResourceTemplate();
|
|
case "user":
|
|
return createUserResourceTemplate();
|
|
case "numbered_resource_templates":
|
|
return createNumberedResourceTemplates(Number(get("count")) || 3);
|
|
case "rfc6570_templates":
|
|
return createRfc6570ResourceTemplates();
|
|
default:
|
|
throw new Error(`Unknown resource template preset: ${name}`);
|
|
}
|
|
}
|
|
|
|
function resolvePromptPreset(
|
|
name: string,
|
|
params?: Record<string, unknown>,
|
|
): PromptDefinition | PromptDefinition[] {
|
|
const p = params ?? {};
|
|
const get = (k: string) => p[k] as unknown;
|
|
switch (name) {
|
|
case "simple_prompt":
|
|
return createSimplePrompt();
|
|
case "args_prompt":
|
|
return createArgsPrompt();
|
|
case "numbered_prompts":
|
|
return createNumberedPrompts(Number(get("count")) || 3);
|
|
default:
|
|
throw new Error(`Unknown prompt preset: ${name}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a preset by type and name to definition(s)
|
|
*/
|
|
export function resolvePreset(
|
|
type: PresetType,
|
|
name: string,
|
|
params?: Record<string, unknown>,
|
|
): PresetResult {
|
|
switch (type) {
|
|
case "tool":
|
|
return resolveToolPreset(name, params);
|
|
case "resource":
|
|
return resolveResourcePreset(name, params);
|
|
case "resourceTemplate":
|
|
return resolveResourceTemplatePreset(name, params);
|
|
case "prompt":
|
|
return resolvePromptPreset(name, params);
|
|
default:
|
|
throw new Error(`Unknown preset type: ${type}`);
|
|
}
|
|
}
|