/** * The Cotal tool surface for a REMOTE MCP server: one server, many conversations. * * A stdio connector is one process per session, so its tools reach the mesh through the one * {@link MeshAgent} that process owns. A remote server has no such anchor. A hosted MCP client such * as Claude pools one OAuth bearer and one MCP transport session across every conversation of an * account, so nothing on a request says which conversation sent it. Each conversation therefore * opens its own connection with `cotal_connect` and names it on every later call. A missing or * foreign handle refuses and runs nothing. No call falls back to the account's last, only or most * recent connection, because that would put one conversation's words under another's identity. * * The embedder owns everything account-shaped through {@link GatewayHost}: authentication, * credential issuance and revocation, and durable records. This module owns the surface, the name * rule and, through {@link handleGatewayRequest}, the MCP server itself, so an embedder never needs * an MCP SDK of its own. */ import type { IncomingMessage, ServerResponse } from "node:http"; import type { Readable, Writable } from "node:stream"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; import { type ConversationPending, type ConversationSelection } from "@cotal-ai/core"; import type { MeshAgent, NativeTurnParticipationLease } from "./agent.js"; import type { AgentConfig } from "./config.js"; import { type RenderedInboxPreview } from "./framing.js"; /** Thrown by {@link GatewayHost.open} when, and only when, the issuer reports that the name already * exists. It is the one failure that moves `cotal_connect` to the next candidate name. An auth, * quota, permission or network failure must be any other error, so it reaches the caller instead * of being retried under a different name. */ export declare class NameTaken extends Error { readonly taken: string; constructor(taken: string); } /** An issuer is waiting for the person to review permissions. This is not a connection or an * authorization; only the authority's authenticated approval page can record the decision. */ export declare class ApprovalRequired extends Error { readonly pending: ConversationPending; constructor(pending: ConversationPending); } /** One live connection: a mesh identity serving one conversation. */ export interface GatewayConnection { readonly id: string; readonly agent: MeshAgent; readonly config: AgentConfig; /** The tools this connection may call, when its host grants it more than {@link GATEWAY_TOOLS}. * Only tools the embedder registers count (see {@link GatewayOptions.tools}). */ readonly tools?: ReadonlySet; } /** What the embedding service supplies. Every method receives the authenticated account, never a * value a tool argument chose. */ export interface GatewayHost { /** The account the request authenticated as. Throw when the request carries no valid * authentication; the tool then runs nothing. */ account(auth: AuthInfo | undefined): string; /** Open a fresh connection for `account` named exactly `name`. Throw {@link NameTaken} only for * an authoritative name collision. The collision check and the issue must be one atomic step per * account: an `open` racing another for the same name throws `NameTaken` rather than also * succeeding, or two conversations end up sharing one name. Error messages reach the caller, * so they must not carry secrets. `selection` is only a proposal, never the person's consent. * Throw {@link ApprovalRequired} until the person approves through the authority's own UI; issue * only their approved selection within their current access. `approvalId` is an untrusted resume * handle, never proof of approval. Bind it to this account, name and unchanged request, and do not * reuse an issued approval for another connection. `space`, when the native schema offers it, is * the registered mesh the caller named. Omit it to keep the embedder's default resolution. A * present value is already a non-blank token; resolve that mesh or refuse, and never substitute * another space's pending or live identity. */ open(account: string, name: string, context?: string, selection?: ConversationSelection, approvalId?: string, space?: string): Promise; /** The connection `id` if `account` owns it, otherwise undefined. Never a default. Throw, with the * reason and the retry, when `account` owns `id` but it can no longer serve, such as a * disconnected connection whose cleanup is unfinished. */ find(account: string, id: string): Promise; /** Stop and release one connection `account` owns. False when it owns no such connection. */ close(account: string, id: string): Promise; /** Authorize or renew finite workflow-turn participation for this exact personal connection. * Omit to offer no participation tool. The host must obtain this lease from manager/auth * authority, not construct authority from caller input. */ participate?(account: string, connection: GatewayConnection): Promise; } /** The mesh tools a remote conversation may call. Process and seat authority (spawn, despawn, * persona, run, yield, reconnect) belongs to a supervised local seat and is not offered here; a * native embedder may offer part of it per connection with {@link NATIVE_GATEWAY_TOOLS}. */ export declare const GATEWAY_TOOLS: ReadonlySet; /** What a native embedder may register beyond {@link GATEWAY_TOOLS}: process, workflow and * reconnect tools. A connection calls one only when its host lists it in * {@link GatewayConnection.tools}, and the manager or endpoint that serves the call still decides * it from the connection's own grant. `cotal_yield` is not here: a conversation is not a managed * seat, so no run turn is ever addressed to it. */ export declare const NATIVE_GATEWAY_TOOLS: ReadonlySet; /** How many names `cotal_connect` tries: the requested one, then `_2` up to this suffix. */ export declare const GATEWAY_NAME_ATTEMPTS = 20; /** The longest `cotal_wait` holds one call when the embedder sets no bound, in seconds. A held call * has to end before anything in front of the gateway does. The reply is one JSON body, so its first * byte goes out when the wait ends: Claude Code gives a remote server 60 seconds to that byte, * Cloudflare ends an origin response after about 100, and claude.ai ends a tool call after 240. */ export declare const GATEWAY_WAIT_SECONDS = 50; /** How an embedder tunes the surface for what sits in front of it. */ export interface GatewayOptions { /** A native embedder binds the connect call to a host-observed conversation. The value is only * a routing key, never authentication; the host must validate its own binding before issue. */ connectContext?: boolean; /** The tools to register, within {@link NATIVE_GATEWAY_TOOLS}. Defaults to {@link GATEWAY_TOOLS}. * Registering a tool offers it to no connection by itself: see {@link GatewayConnection.tools}. */ tools?: ReadonlySet; /** `cotal_connect` takes the channels, role and capabilities a connection requests * within the person's own access, and hands the canonical selection to {@link GatewayHost.open}. */ selection?: boolean; /** A native embedder accepts an optional registered space name and passes it to * {@link GatewayHost.open}. Hosted OAuth is bound to one space, so this stays off there. */ space?: boolean; /** Offer an `approval_id` resume handle for hosted permission requests. The host must validate * its exact request binding and record the person's decision independently of this handle. */ approvalResume?: boolean; /** The longest `cotal_wait` may hold one call, in whole seconds. Defaults to * {@link GATEWAY_WAIT_SECONDS}. Keep it under every proxy and client timeout on the path. */ waitSeconds?: number; /** A way to wake the client other than a held `cotal_wait`, for an embedder whose client can be * woken. Without it a conversation hears messages only when it calls a tool. */ wake?: GatewayWake; } /** How an embedder wakes its client when a connection has something to read. */ export interface GatewayWake { /** Called when a connection gets something that ends a `cotal_wait` while none holds it, and * not again until a read or explicit settlement rearms that connection. A true return means * a reader heard the signal, not that content was admitted or acknowledged. */ signal(connection: GatewayConnection, preview: RenderedInboxPreview): boolean; /** What `cotal_connect` adds about listening, read when the connection opens: whether this * client will be woken. */ listening(connection: GatewayConnection): string; /** Whether other tool results should recommend an inbox read. A full-content native reader can * suppress that advice while it owns delivery; notification-only clients keep the default. */ inboxNotice?(connection: GatewayConnection): boolean; /** Called once with the rearm callback. With no connection, reoffer unread work to a replacement * reader. A settled inline handoff rearms only its own connection; false clears its latch without * immediately retrying a refused delivery. */ attach?(rewake: (connection?: GatewayConnection, fireNow?: boolean) => void): void; } /** The names `cotal_connect` tries for `base`, in order. */ export declare function gatewayNames(base: string, attempts?: number): string[]; /** * Register the remote surface on `server`: `cotal_connect`, `cotal_disconnect`, `cotal_wait`, and * each tool in {@link GATEWAY_TOOLS} with a required `connection_id`. * * Each call is parsed and run against the spec built from that connection's own configuration, so a * connection's channels and grants decide what the call does. * * A remote MCP client gives the server no way to start a turn, so a conversation hears a message * only when it calls a tool. `cotal_wait` is the call that listens: it holds until a message * arrives and then returns it, and every other routed tool says when messages are waiting. */ export declare function registerCotalGatewayTools(server: McpServer, host: GatewayHost, source?: string, options?: GatewayOptions): void; /** How a conversation hears messages when nothing can wake it: the last part of * {@link GATEWAY_INSTRUCTIONS}. */ export declare const GATEWAY_LISTENING: string; /** How a conversation joins and names its connection: the first part of {@link GATEWAY_INSTRUCTIONS}. */ export declare const GATEWAY_CONNECTING: string; /** The server instructions {@link handleGatewayRequest} sends when the embedder gives none. */ export declare const GATEWAY_INSTRUCTIONS = "Cotal connects this conversation to the person's Cotal space after they approve its permissions. Call cotal_connect with a short name. If approval is pending, show the person its link and wait for their decision; do not approve for them or poll. Then resume the same request as directed. Only a successful connection returns connection_id, required on every other cotal tool call. Each conversation needs its own connection. Never reuse another conversation's connection_id or approval_id. Messages reach you only when you call a tool: to listen, call cotal_wait, which returns as soon as a message arrives. When the person asks you to stay on Cotal or keep listening, call cotal_wait again each time it returns and handle each message as it arrives, until they ask you to stop."; /** One HTTP request to a gateway endpoint. */ export interface GatewayRequest { req: IncomingMessage; res: ServerResponse; /** The parsed JSON body, for a framework that already read the request stream. Omit it and the * body is read from `req`. */ body?: unknown; /** What the embedder authenticated this request as, handed to {@link GatewayHost.account}. * Without it every tool refuses and runs nothing. */ auth?: AuthInfo; /** The source tag on messages this endpoint sends. */ source?: string; instructions?: string; /** See {@link GatewayOptions.connectContext}. */ connectContext?: boolean; /** See {@link GatewayOptions.approvalResume}. */ approvalResume?: boolean; /** See {@link GatewayOptions.selection}. */ selection?: boolean; /** See {@link GatewayOptions.waitSeconds}. */ waitSeconds?: number; } /** * Serve one HTTP request to a gateway endpoint with this package's own MCP server and Streamable * HTTP transport. The embedder authenticates and routes. It never builds an MCP server, so its own * SDK or zod version cannot disagree with the one these tools were written against. * * The endpoint is stateless. Each POST gets a fresh server and transport and a JSON response, so * nothing survives between requests except what the host stores. There is no server-initiated * stream and no session to end, so every other method answers 405, as the Streamable HTTP * transport allows. A `cotal_wait` call holds its POST open for up to `waitSeconds`. */ export declare function handleGatewayRequest(host: GatewayHost, r: GatewayRequest): Promise; /** One local client that launches the gateway as a process and speaks MCP over its pipes. */ export interface GatewayStdio { /** Defaults to `process.stdin`. */ input?: Readable; /** Defaults to `process.stdout`, so every diagnostic must go to stderr. */ output?: Writable; /** The source tag on messages this server sends. */ source?: string; instructions?: string; /** See {@link GatewayOptions.connectContext}. */ connectContext?: boolean; /** See {@link GatewayOptions.tools}. */ tools?: ReadonlySet; /** See {@link GatewayOptions.selection}. */ selection?: boolean; /** See {@link GatewayOptions.space}. */ space?: boolean; /** See {@link GatewayOptions.approvalResume}. */ approvalResume?: boolean; /** See {@link GatewayOptions.waitSeconds}. */ waitSeconds?: number; /** See {@link GatewayOptions.wake}. */ wake?: GatewayWake; } /** * Serve the gateway surface over stdio with this package's own MCP server, for a client such as * Claude Desktop that runs one local process for every conversation. A stdio request carries no * authentication, so `host.account` receives `undefined` and names the local user. A client that * cancels a call aborts its signal, as a closed HTTP request does. */ export declare function serveGatewayStdio(host: GatewayHost, o?: GatewayStdio): Promise<{ close(): Promise; }>; //# sourceMappingURL=gateway.d.ts.map