feat(web): URL elicitation — explicit completion step + error-path (-32042) handling (#1415) (#1436)
* feat(web): two-step URL elicitation completion (#1415) Replace accept-on-open with an explicit completion step for URL elicitations. Opening the URL now only transitions the panel to its waiting state; the elicitation resolves with `accept` only when the user clicks the revealed "I've completed it" action, or with `cancel`. - ElicitationUrlPanel: add `onComplete`, reveal the completion button and relabel open→"Reopen in Browser" while `isWaiting`. - PendingClientRequestModal: wire local `isWaiting`; open no longer sends a response. Removed the accept-on-open comment that pointed here. - Tests cover open→waiting→complete and open→cancel, and assert opening alone does not resolve the elicitation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): handle URL-elicitation-required (-32042) error path Implements the spec's "URL mode with elicitation required error" flow (2025-11-25). When a tools/call returns a `-32042` UrlElicitationRequired error, the client now surfaces the required URL elicitation(s) and retries the call once completed, instead of showing a bare error. Core (InspectorClient.callTool): - Detect `-32042` (typed UrlElicitationRequiredError or generic McpError) via the new `getUrlElicitationsFromError` helper. - Surface each carried elicitation in order through the existing pending- elicitation queue / modal (reusing the two-step "Open → I've completed it" UI), then re-issue the original call once all are accepted. A decline/cancel aborts with a clear message; a bounded retry count guards a server that keeps returning the error. - `ElicitationCreateMessage.completeIfPending()` lets the optional `notifications/elicitation/complete` auto-advance/auto-accept an open URL elicitation; the completion-notification handler now resolves (was: only removed) the pending entry. - Extracted `attemptToolCall` / `dispatchFailedToolCall` from the old callTool body to support the retry loop without duplicating bookkeeping. Web (App): - A non-spec `-32042` with no elicitations (nothing to open) shows a dedicated yellow toast linking to a new UrlElicitationErrorModal with the raw error body, mirroring the output-schema-validation toast. Tests: helper unit tests, completeIfPending unit tests, callTool error-path retry/cancel/no-list/ordinary-error tests (injected fake client), the new modal (test + stories), and an App toast test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): abort URL-elicitation error path on a repeated URL Guard the -32042 retry loop: if the server's retry error re-requests a URL the user already completed earlier in the same call, completing it again can't make progress, so callTool aborts with a new typed UrlElicitationLoopError instead of re-prompting (which would loop until the retry cap). The web layer detects it and shows a "URL elicitation loop" toast naming the repeated URL rather than a generic failure. Tests: core abort-on-repeat test (URL presented once, two attempts, then loop error) and an App loop-toast test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): let Copy-URL reach the URL-elicitation completion step Address review of #1436: the "I've completed it" action was gated behind clicking "Open in Browser" (only that set isWaiting), so a user who used Copy URL to finish the flow externally could only Cancel. Copying the URL now also moves the panel into the waiting state, exposing the completion step. Also trims the now-redundant second sentence of the waiting hint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(core): address review of error-path URL elicitation Review follow-ups on #1436 (all non-blocking): - Remove the leftover bare block scope in attemptToolCall (extraction artifact); body de-indented one level. - Log a warning when callTool gives up after the URL-elicitation retry cap is hit, so a server that keeps demanding new elicitations is diagnosable rather than looking like an ordinary failure. - Assert the dispatch-once invariant on the loop-abort and cancel-abort paths (exactly one failed toolCallResultChange event). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(core): fix attemptToolCall body indentation Run prettier over the extracted attemptToolCall method body, which was left over-indented by 2 spaces after the bare-block removal in 0754b43d. core/ is outside clients/web's format:check scope, so npm run validate did not catch it. Whitespace-only; the pre-existing line 658 wrap (also outside the format scope) is left as-is, out of scope for this PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(core): settle pending elicitations on disconnect to avoid callTool hang Address review of #1436: an error-path `awaitUrlElicitation` promise only settled via respond, but `disconnect()` cleared `pendingElicitations` wholesale — so a disconnect mid-elicitation (server crash, manual disconnect, transport close) left `callTool` (and the UI's in-flight toolCallState) hanging forever. - Add `ElicitationCreateMessage.cancel()`: settles a pending elicitation as `{ action: "cancel" }` without calling `onRemove` (the queue is cleared by the caller, so it must not splice mid-iteration). - `disconnect()` now cancels each pending elicitation before clearing the array, and dispatches `pendingElicitationsChange` (it already did so for samples) so the modal closes on a transport-close teardown too. Tests: disconnect-mid-elicitation settles callTool (no hang); cancel() unit tests (resolves-without-remove, no-op once responded). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
49d6be6987
commit
5cabc1ecd7
1 changed files with 1232 additions and 124 deletions
|
|
@ -56,7 +56,9 @@ import type {
|
|||
Prompt,
|
||||
Root,
|
||||
CreateMessageResult,
|
||||
ElicitRequest,
|
||||
ElicitResult,
|
||||
ElicitRequestURLParams,
|
||||
CallToolResult,
|
||||
Task,
|
||||
Progress,
|
||||
|
|
@ -110,6 +112,10 @@ import {
|
|||
} from "./inspectorClientEventTarget.js";
|
||||
import { SamplingCreateMessage } from "./samplingCreateMessage.js";
|
||||
import { ElicitationCreateMessage } from "./elicitationCreateMessage.js";
|
||||
import {
|
||||
getUrlElicitationsFromError,
|
||||
UrlElicitationLoopError,
|
||||
} from "./urlElicitation.js";
|
||||
import type { AuthGuidedState, OAuthStep } from "../auth/types.js";
|
||||
import type { OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
||||
import type pino from "pino";
|
||||
|
|
@ -126,6 +132,14 @@ interface ReceiverTaskRecord {
|
|||
cleanupTimeoutId?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cap on how many times a single `callTool` will surface URL elicitations and
|
||||
* retry after a `-32042` (UrlElicitationRequired) response. A spec-compliant
|
||||
* flow resolves in one round; the bound only guards against a server that keeps
|
||||
* returning the error.
|
||||
*/
|
||||
const MAX_URL_ELICITATION_RETRIES = 5;
|
||||
|
||||
/**
|
||||
* InspectorClient wraps an MCP Client and provides:
|
||||
* - Message tracking and storage
|
||||
|
|
@ -938,7 +952,11 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
e.request.params?.elicitationId === elicitationId,
|
||||
);
|
||||
if (pending) {
|
||||
pending.remove();
|
||||
// Resolve (not just remove): for the error-path retry loop this
|
||||
// unblocks `awaitUrlElicitation`, and for request-path it sends
|
||||
// the `accept` response the server is still awaiting. No-op once
|
||||
// the user already clicked "I've completed it".
|
||||
pending.completeIfPending();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -1005,8 +1023,14 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
this.dispatchTypedEvent("disconnect");
|
||||
}
|
||||
|
||||
// Clear server state on disconnect (list state is in state managers)
|
||||
// Clear server state on disconnect (list state is in state managers).
|
||||
// Settle any outstanding elicitations as cancelled before dropping them, so
|
||||
// an error-path `awaitUrlElicitation` (which blocks `callTool`) doesn't hang
|
||||
// forever when the queue is cleared on teardown.
|
||||
this.pendingSamples = [];
|
||||
for (const elicitation of this.pendingElicitations) {
|
||||
elicitation.cancel();
|
||||
}
|
||||
this.pendingElicitations = [];
|
||||
// Clear resource subscriptions on disconnect
|
||||
this.subscribedResources.clear();
|
||||
|
|
@ -1022,6 +1046,7 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
this.serverInfo = undefined;
|
||||
this.instructions = undefined;
|
||||
this.dispatchTypedEvent("pendingSamplesChange", this.pendingSamples);
|
||||
this.dispatchTypedEvent("pendingElicitationsChange", this.pendingElicitations);
|
||||
this.dispatchTypedEvent("capabilitiesChange", this.capabilities);
|
||||
this.dispatchTypedEvent("serverInfoChange", this.serverInfo);
|
||||
this.dispatchTypedEvent("instructionsChange", this.instructions);
|
||||
|
|
@ -1330,124 +1355,276 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
try {
|
||||
let convertedArgs: Record<string, JsonValue> = args;
|
||||
const stringArgs: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
if (typeof value === "string") {
|
||||
stringArgs[key] = value;
|
||||
// Retry loop for the URL-elicitation error path: a `-32042`
|
||||
// (UrlElicitationRequired) response means the server needs the user to
|
||||
// complete one or more URL elicitations before the call can succeed. We
|
||||
// surface them, wait for completion, then re-issue the same call. The
|
||||
// counter bounds a server that keeps returning `-32042` so we can't spin
|
||||
// forever (each accepted round is one attempt).
|
||||
let urlElicitationAttempt = 0;
|
||||
// URLs already presented in this call. If a retry's error re-requests one,
|
||||
// completing it again can't make progress (the server isn't advancing), so
|
||||
// we abort with a UrlElicitationLoopError rather than re-prompt the user.
|
||||
const presentedUrls = new Set<string>();
|
||||
while (true) {
|
||||
try {
|
||||
return await this.attemptToolCall(
|
||||
tool,
|
||||
args,
|
||||
generalMetadata,
|
||||
toolSpecificMetadata,
|
||||
taskOptions,
|
||||
options,
|
||||
);
|
||||
} catch (error) {
|
||||
const urlElicitations = getUrlElicitationsFromError(error);
|
||||
if (
|
||||
urlElicitations &&
|
||||
urlElicitations.length > 0 &&
|
||||
urlElicitationAttempt < MAX_URL_ELICITATION_RETRIES
|
||||
) {
|
||||
// Loop guard: the server repeated a URL we already handled this call.
|
||||
const repeated = urlElicitations.find((e) =>
|
||||
presentedUrls.has(e.url),
|
||||
);
|
||||
if (repeated) {
|
||||
const loopError = new UrlElicitationLoopError(repeated.url);
|
||||
this.dispatchFailedToolCall(
|
||||
tool,
|
||||
args,
|
||||
generalMetadata,
|
||||
toolSpecificMetadata,
|
||||
loopError.message,
|
||||
);
|
||||
throw loopError;
|
||||
}
|
||||
urlElicitationAttempt++;
|
||||
for (const e of urlElicitations) {
|
||||
presentedUrls.add(e.url);
|
||||
}
|
||||
const action = await this.runUrlElicitations(urlElicitations);
|
||||
if (action === "accept") {
|
||||
continue;
|
||||
}
|
||||
// The user declined/cancelled a required URL elicitation, so the
|
||||
// original call can't proceed. Surface it as a failed call with a
|
||||
// clear reason instead of the raw "-32042" message.
|
||||
const abortError = new Error(
|
||||
`Tool call cancelled: required URL elicitation was ${
|
||||
action === "decline" ? "declined" : "cancelled"
|
||||
}.`,
|
||||
);
|
||||
this.dispatchFailedToolCall(
|
||||
tool,
|
||||
args,
|
||||
generalMetadata,
|
||||
toolSpecificMetadata,
|
||||
abortError.message,
|
||||
);
|
||||
throw abortError;
|
||||
}
|
||||
// Not a URL-elicitation error (or the non-spec no-list variant, or
|
||||
// retries exhausted): record + rethrow so the caller can surface it.
|
||||
// The App distinguishes the no-list `-32042` case (a dedicated toast)
|
||||
// via getUrlElicitationsFromError on the thrown error.
|
||||
if (urlElicitations && urlElicitations.length > 0) {
|
||||
// A non-empty list here means the retry cap was hit (the live path
|
||||
// returns or continues). Log the give-up so a server that keeps
|
||||
// demanding new URL elicitations is diagnosable rather than looking
|
||||
// like an ordinary failure.
|
||||
this.logger.warn(
|
||||
{ tool: tool.name, attempts: urlElicitationAttempt },
|
||||
`Tool "${tool.name}" still required URL elicitations after ${MAX_URL_ELICITATION_RETRIES} attempts; giving up.`,
|
||||
);
|
||||
}
|
||||
this.dispatchFailedToolCall(
|
||||
tool,
|
||||
args,
|
||||
generalMetadata,
|
||||
toolSpecificMetadata,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
if (Object.keys(stringArgs).length > 0) {
|
||||
const convertedStringArgs = convertToolParameters(tool, stringArgs);
|
||||
convertedArgs = { ...args, ...convertedStringArgs };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single tools/call attempt: convert args, issue the request, validate,
|
||||
* and return a successful {@link ToolCallInvocation}. Throws on any error
|
||||
* (including a `-32042` UrlElicitationRequired response); {@link callTool}'s
|
||||
* retry loop owns the elicitation handling and failure bookkeeping.
|
||||
*/
|
||||
private async attemptToolCall(
|
||||
tool: Tool,
|
||||
args: Record<string, JsonValue>,
|
||||
generalMetadata?: Record<string, string>,
|
||||
toolSpecificMetadata?: Record<string, string>,
|
||||
taskOptions?: { ttl?: number },
|
||||
options?: { skipOutputValidation?: boolean },
|
||||
): Promise<ToolCallInvocation> {
|
||||
const client = this.client;
|
||||
if (!client) {
|
||||
throw new Error("Client is not connected");
|
||||
}
|
||||
let convertedArgs: Record<string, JsonValue> = args;
|
||||
const stringArgs: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
if (typeof value === "string") {
|
||||
stringArgs[key] = value;
|
||||
}
|
||||
}
|
||||
if (Object.keys(stringArgs).length > 0) {
|
||||
const convertedStringArgs = convertToolParameters(tool, stringArgs);
|
||||
convertedArgs = { ...args, ...convertedStringArgs };
|
||||
}
|
||||
|
||||
// Merge general metadata with tool-specific metadata; tool-specific wins.
|
||||
const callMetadata: Record<string, string> | undefined =
|
||||
generalMetadata || toolSpecificMetadata
|
||||
? { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}) }
|
||||
: undefined;
|
||||
|
||||
const timestamp = new Date();
|
||||
// Fold in this client's defaultMetadata so server-wide _meta reaches
|
||||
// the wire even when the caller passed nothing.
|
||||
const metadata = this.mergeMeta(callMetadata);
|
||||
|
||||
const callParams: {
|
||||
name: string;
|
||||
arguments: Record<string, JsonValue>;
|
||||
_meta?: Record<string, string>;
|
||||
task?: { ttl: number };
|
||||
} = {
|
||||
name: tool.name,
|
||||
arguments: convertedArgs,
|
||||
_meta: metadata,
|
||||
};
|
||||
if (taskOptions?.ttl != null) {
|
||||
callParams.task = { ttl: taskOptions.ttl };
|
||||
}
|
||||
|
||||
// MCP Apps forward the server's CallToolResult straight to the running
|
||||
// view, which is the real consumer. The SDK's callTool() validates
|
||||
// structuredContent against the tool's outputSchema and THROWS on a
|
||||
// mismatch — which would deny the app a result the server actually
|
||||
// returned (and that legacy hosts render fine). For those passthrough
|
||||
// calls go through request() directly, which skips that host-side
|
||||
// validation. Regular Tools-screen calls keep validating.
|
||||
const requestOptions = this.getRequestOptions(metadata?.progressToken);
|
||||
// Both branches yield a CallToolResult: request() parsed it with
|
||||
// CallToolResultSchema above, callTool() returns the same shape — so the
|
||||
// `as CallToolResult` casts below are safe.
|
||||
const result = options?.skipOutputValidation
|
||||
? await this.client.request(
|
||||
{ method: "tools/call", params: callParams },
|
||||
CallToolResultSchema,
|
||||
requestOptions,
|
||||
)
|
||||
: await this.client.callTool(callParams, undefined, requestOptions);
|
||||
|
||||
// On the bypass path the result was delivered without the SDK's strict
|
||||
// output validation. Run that check ourselves, non-fatally, so callers can
|
||||
// warn that strict clients would reject this payload (the app still
|
||||
// renders, but it may not in other hosts).
|
||||
const outputValidationError = options?.skipOutputValidation
|
||||
? this.validateToolOutput(tool, result as CallToolResult)
|
||||
// Merge general metadata with tool-specific metadata; tool-specific wins.
|
||||
const callMetadata: Record<string, string> | undefined =
|
||||
generalMetadata || toolSpecificMetadata
|
||||
? { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}) }
|
||||
: undefined;
|
||||
|
||||
const invocation: ToolCallInvocation = {
|
||||
toolName: tool.name,
|
||||
params: args,
|
||||
result: result as CallToolResult,
|
||||
timestamp,
|
||||
success: true,
|
||||
metadata,
|
||||
outputValidationError,
|
||||
};
|
||||
const timestamp = new Date();
|
||||
// Fold in this client's defaultMetadata so server-wide _meta reaches
|
||||
// the wire even when the caller passed nothing.
|
||||
const metadata = this.mergeMeta(callMetadata);
|
||||
|
||||
this.dispatchTypedEvent("toolCallResultChange", {
|
||||
toolName: tool.name,
|
||||
params: args,
|
||||
result: invocation.result,
|
||||
timestamp,
|
||||
success: true,
|
||||
metadata,
|
||||
outputValidationError,
|
||||
});
|
||||
|
||||
return invocation;
|
||||
} catch (error) {
|
||||
// Merge general metadata with tool-specific metadata for error case
|
||||
const callMetadata: Record<string, string> | undefined =
|
||||
generalMetadata || toolSpecificMetadata
|
||||
? { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}) }
|
||||
: undefined;
|
||||
|
||||
const timestamp = new Date();
|
||||
const metadata = this.mergeMeta(callMetadata);
|
||||
|
||||
const invocation: ToolCallInvocation = {
|
||||
toolName: tool.name,
|
||||
params: args,
|
||||
result: null,
|
||||
timestamp,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
metadata,
|
||||
};
|
||||
|
||||
this.dispatchTypedEvent("toolCallResultChange", {
|
||||
toolName: tool.name,
|
||||
params: args,
|
||||
result: null,
|
||||
timestamp,
|
||||
success: false,
|
||||
error: invocation.error,
|
||||
metadata,
|
||||
});
|
||||
|
||||
throw error;
|
||||
const callParams: {
|
||||
name: string;
|
||||
arguments: Record<string, JsonValue>;
|
||||
_meta?: Record<string, string>;
|
||||
task?: { ttl: number };
|
||||
} = {
|
||||
name: tool.name,
|
||||
arguments: convertedArgs,
|
||||
_meta: metadata,
|
||||
};
|
||||
if (taskOptions?.ttl != null) {
|
||||
callParams.task = { ttl: taskOptions.ttl };
|
||||
}
|
||||
|
||||
// MCP Apps forward the server's CallToolResult straight to the running
|
||||
// view, which is the real consumer. The SDK's callTool() validates
|
||||
// structuredContent against the tool's outputSchema and THROWS on a
|
||||
// mismatch — which would deny the app a result the server actually
|
||||
// returned (and that legacy hosts render fine). For those passthrough
|
||||
// calls go through request() directly, which skips that host-side
|
||||
// validation. Regular Tools-screen calls keep validating.
|
||||
const requestOptions = this.getRequestOptions(metadata?.progressToken);
|
||||
// Both branches yield a CallToolResult: request() parsed it with
|
||||
// CallToolResultSchema above, callTool() returns the same shape — so the
|
||||
// `as CallToolResult` casts below are safe.
|
||||
const result = options?.skipOutputValidation
|
||||
? await client.request(
|
||||
{ method: "tools/call", params: callParams },
|
||||
CallToolResultSchema,
|
||||
requestOptions,
|
||||
)
|
||||
: await client.callTool(callParams, undefined, requestOptions);
|
||||
|
||||
// On the bypass path the result was delivered without the SDK's strict
|
||||
// output validation. Run that check ourselves, non-fatally, so callers can
|
||||
// warn that strict clients would reject this payload (the app still
|
||||
// renders, but it may not in other hosts).
|
||||
const outputValidationError = options?.skipOutputValidation
|
||||
? this.validateToolOutput(tool, result as CallToolResult)
|
||||
: undefined;
|
||||
|
||||
const invocation: ToolCallInvocation = {
|
||||
toolName: tool.name,
|
||||
params: args,
|
||||
result: result as CallToolResult,
|
||||
timestamp,
|
||||
success: true,
|
||||
metadata,
|
||||
outputValidationError,
|
||||
};
|
||||
|
||||
this.dispatchTypedEvent("toolCallResultChange", {
|
||||
toolName: tool.name,
|
||||
params: args,
|
||||
result: invocation.result,
|
||||
timestamp,
|
||||
success: true,
|
||||
metadata,
|
||||
outputValidationError,
|
||||
});
|
||||
|
||||
return invocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a failed tools/call as a `toolCallResultChange` event (history + the
|
||||
* Tools panel) without throwing. {@link callTool} calls this before rethrowing
|
||||
* so a failure — whether a transport error, a declined URL elicitation, or a
|
||||
* non-spec `-32042` — lands in the request history exactly once.
|
||||
*/
|
||||
private dispatchFailedToolCall(
|
||||
tool: Tool,
|
||||
args: Record<string, JsonValue>,
|
||||
generalMetadata: Record<string, string> | undefined,
|
||||
toolSpecificMetadata: Record<string, string> | undefined,
|
||||
errorMessage: string,
|
||||
): void {
|
||||
const callMetadata: Record<string, string> | undefined =
|
||||
generalMetadata || toolSpecificMetadata
|
||||
? { ...(generalMetadata || {}), ...(toolSpecificMetadata || {}) }
|
||||
: undefined;
|
||||
const metadata = this.mergeMeta(callMetadata);
|
||||
this.dispatchTypedEvent("toolCallResultChange", {
|
||||
toolName: tool.name,
|
||||
params: args,
|
||||
result: null,
|
||||
timestamp: new Date(),
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface the URL elicitations carried by a `-32042` error, one at a time and
|
||||
* in order (per the spec's "URL mode with elicitation required error" flow),
|
||||
* returning as soon as the user declines/cancels one. Returns `"accept"` only
|
||||
* when every elicitation was accepted, which is {@link callTool}'s signal to
|
||||
* retry the original call.
|
||||
*/
|
||||
private async runUrlElicitations(
|
||||
elicitations: ElicitRequestURLParams[],
|
||||
): Promise<ElicitResult["action"]> {
|
||||
for (const params of elicitations) {
|
||||
const action = await this.awaitUrlElicitation(params);
|
||||
if (action !== "accept") {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return "accept";
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one error-path URL elicitation to the pending queue (so it renders in
|
||||
* the same modal as request-path elicitations) and resolve with the user's
|
||||
* action. Unlike the request-path handler there is no server request to
|
||||
* answer — accepting it just unblocks the retry; the server's optional
|
||||
* `notifications/elicitation/complete` resolves it as accepted too (via
|
||||
* `completeIfPending`).
|
||||
*/
|
||||
private awaitUrlElicitation(
|
||||
params: ElicitRequestURLParams,
|
||||
): Promise<ElicitResult["action"]> {
|
||||
return new Promise<ElicitResult["action"]>((resolve) => {
|
||||
const request = {
|
||||
method: "elicitation/create",
|
||||
params,
|
||||
} as ElicitRequest;
|
||||
const message = new ElicitationCreateMessage(
|
||||
request,
|
||||
(result) => resolve(result.action),
|
||||
(id) => this.removePendingElicitation(id),
|
||||
);
|
||||
this.addPendingElicitation(message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue