Skip to Content
Elysia MCP Adapterelysia-mcp-adapter
Core Concepts

Messaging and Input

Choose the right channel for progress, client input, durable work, and MCP Apps.

Use caseAPIWhere the update goes
Show progress during a callcontext.notify.progress(value, { total, message })The original HTTP response stream, when the client sends a progress token
Send a compatibility log messagecontext.notify.message(data, { level, logger })The original response stream, if both peers opt in
Ask the client for inputreturn context.input.require(requests)The final result of the current request; the client retries
Report durable task statusawait execution.notify.status(message)The task snapshot, available to polling clients and task subscribers
Ask for input during a taskawait execution.input.request(key, request)The task snapshot; the client answers through tasks/update
Tell listeners about changespublisher.resourceUpdated(uri) or a list-change methodAn authorized subscriptions/listen stream
Deliver domain eventsEvents providerThe selected experimental Events profile: replay polling, event SSE, or verified webhooks
Send a message from an App viewApps SDK host bridgeThe host conversation, if the host supports it

Live calls

Native tools, resource readers, and prompts receive notify. Route-backed handlers can get the same context with getMcpInvocationContext(request). Ordinary HTTP calls have no MCP context.

app.get(
  "/index",
  ({ request }) => {
    const notify = getMcpInvocationContext(request)?.notify;
    notify?.progress(1, { total: 2, message: "Scanning" });
    notify?.message("Index scan complete", { logger: "indexer" });
    notify?.progress(2, { total: 2, message: "Done" });
    return "indexed";
  },
  { mcp: { name: "index" } },
);

Send finite, strictly increasing progress values. Use the progress message for concise, user-facing status. A client that does not request a channel still receives the final result. If a client disconnects, stop work through context.signal. The adapter stops sending and bounds queued notifications to 128 frames and 64 KiB per response. The final response does not count toward that budget.

notifications/message is deprecated. Set core.logging: true for compatible modern clients that send io.modelcontextprotocol/logLevel in request metadata. The default message level is info; the adapter sends only messages at or above the requested level. Never include secrets or sensitive data. Use application logging or telemetry for operational diagnostics.

Request input and resume

Build requests with mcpInput.form(), mcpInput.url(), mcpInput.sampling(), or mcpInput.roots(). Use context.input.supports(request) to select a fallback for clients without the necessary capability.

const approval = mcpInput.form({
  message: "Run the index?",
  requestedSchema: {
    type: "object",
    properties: { approved: { type: "boolean" } },
    required: ["approved"],
  },
});

app.mcpTool("run-index", (_, { input }) => {
  if (!input.supports(approval)) return "Approval is unavailable.";
  const answer = input.response("approval", approval);
  if (!answer) return input.require({ approval });
  if (answer.action !== "accept" || answer.content?.approved !== true) {
    return "Indexing was skipped.";
  }
  return runIndex();
});

Request only the missing answers on a retry. The adapter validates known answers against the request kind and any form schema. Treat answers as untrusted data, including roots and sampling results. Sampling and roots are deprecated. With URL elicitation, verify the out-of-band operation on your server before proceeding; host acceptance alone does not prove completion. Keep secrets out of form inputs and state.

If a round needs state, pass { state: opaqueReference } to input.require and configure core.continuation.signingKey. The client echoes the protected state unchanged on a new request; the handler reads context.requestState. Signing protects integrity and does not hide the contents. Use opaque references for sensitive data and a shared, single-use provider for state that authorizes a one-time action.

Durable tasks and subscriptions

Use an explicit tool's beforeTask hook to request input before enqueueing. The hook runs only when the call will create a task, after authorization and tool argument validation. For a synchronous fallback, handle input in the tool itself.

Use createMcpTaskExecutionContext inside a worker with the task ID, its original provider context, and provider callbacks. Persist notify.status through the provider's setStatusMessage callback and record input.request through its requestInput callback. The helper checks each request against the capabilities retained with the originating task. Recreate that context from the provider's durable owner context after a worker restart. Resume only when the provider has accepted the necessary answers. The client continues polling or watching task snapshots. The worker does not need the original HTTP stream. Direct calls to the raw task controller have no originating client; they retain the legacy request contract, while modern wire calls enforce the client's declared capabilities.

const publisher = createMcpSubscriptionPublisher();
const plugin = mcp({
  core: {
    subscriptions: { provider: publisher.provider, resources: true, promptsListChanged: true },
  },
});

// Publish after the resource update commits.
publisher.resourceUpdated("docs:///release-notes");

The local publisher coalesces pending updates and disconnects slow listeners on overflow. Clients resubscribe and refetch. For several replicas, use a shared provider to deliver changes across processes. The adapter still checks the subscription filter and resource authorization.

Apps and companion prompts

Use the Apps SDK host bridge to call tools, send chat messages, or update model context from the view. Host capabilities govern delivery. Tool progress and task snapshots do not automatically appear in the iframe.

app.mcpTool("open-board", openBoard, {
  app: { resourceUri: "ui://board/index.html" },
  prompt: {
    name: "board",
    title: "Open my board",
    render: (_, { toolName }) => `Open my board by calling the ${toolName} tool.`,
  },
});

A companion prompt appears in prompts/list for callers who can see the tool. The host may show it as a slash command. Fetching the prompt renders messages; the tool runs only through a normal tool call. Use .mcpPrompt() for independent prompts. App-only tools cannot register companion prompts. Register route tools at startup.

On this page