///
import { Router } from "@ttoss/http-server";
import { AsyncLocalStorage } from "node:async_hooks";
import { CallToolResult, McpServer, McpServer as McpServer$1 } from "@modelcontextprotocol/server";
import { z } from "zod";
import accepts from "accepts";
import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
import Cookies from "cookies";
import { EventEmitter } from "events";
import { IncomingHttpHeaders, IncomingMessage, OutgoingHttpHeaders, Server, ServerResponse } from "http";
import { Http2ServerRequest, Http2ServerResponse } from "http2";
import httpAssert from "http-assert";
import contentDisposition from "content-disposition";
import HttpErrors from "http-errors";
import Keygrip from "keygrip";
import compose from "koa-compose";
import { ListenOptions, Socket } from "net";
import { ParsedUrlQuery } from "querystring";
import * as url from "url";
//#region ../../node_modules/.pnpm/@types+koa@3.0.3/node_modules/@types/koa/index.d.ts
declare interface ContextDelegatedRequest {
/**
* Return request header.
*/
header: IncomingHttpHeaders;
/**
* Return request header, alias as request.header
*/
headers: IncomingHttpHeaders;
/**
* Get/Set request URL.
*/
url: string;
/**
* Get origin of URL.
*/
origin: string;
/**
* Get full request URL.
*/
href: string;
/**
* Get/Set request method.
*/
method: string;
/**
* Get request pathname.
* Set pathname, retaining the query-string when present.
*/
path: string;
/**
* Get parsed query-string.
* Set query-string as an object.
*/
query: ParsedUrlQuery;
/**
* Get/Set query string.
*/
querystring: string;
/**
* Get the search string. Same as the querystring
* except it includes the leading ?.
*
* Set the search string. Same as
* response.querystring= but included for ubiquity.
*/
search: string;
/**
* Parse the "Host" header field host
* and support X-Forwarded-Host when a
* proxy is enabled.
*/
host: string;
/**
* Parse the "Host" header field hostname
* and support X-Forwarded-Host when a
* proxy is enabled.
*/
hostname: string;
/**
* Get WHATWG parsed URL object.
*/
URL: url.URL;
/**
* Check if the request is fresh, aka
* Last-Modified and/or the ETag
* still match.
*/
fresh: boolean;
/**
* Check if the request is stale, aka
* "Last-Modified" and / or the "ETag" for the
* resource has changed.
*/
stale: boolean;
/**
* Check if the request is idempotent.
*/
idempotent: boolean;
/**
* Return the request socket.
*/
socket: Socket;
/**
* Return the protocol string "http" or "https"
* when requested with TLS. When the proxy setting
* is enabled the "X-Forwarded-Proto" header
* field will be trusted. If you're running behind
* a reverse proxy that supplies https for you this
* may be enabled.
*/
protocol: string;
/**
* Short-hand for:
*
* this.protocol == 'https'
*/
secure: boolean;
/**
* Request remote address. Supports X-Forwarded-For when app.proxy is true.
*/
ip: string;
/**
* When `app.proxy` is `true`, parse
* the "X-Forwarded-For" ip address list.
*
* For example if the value were "client, proxy1, proxy2"
* you would receive the array `["client", "proxy1", "proxy2"]`
* where "proxy2" is the furthest down-stream.
*/
ips: string[];
/**
* Return subdomains as an array.
*
* Subdomains are the dot-separated parts of the host before the main domain
* of the app. By default, the domain of the app is assumed to be the last two
* parts of the host. This can be changed by setting `app.subdomainOffset`.
*
* For example, if the domain is "tobi.ferrets.example.com":
* If `app.subdomainOffset` is not set, this.subdomains is
* `["ferrets", "tobi"]`.
* If `app.subdomainOffset` is 3, this.subdomains is `["tobi"]`.
*/
subdomains: string[];
/**
* Check if the given `type(s)` is acceptable, returning
* the best match when true, otherwise `false`, in which
* case you should respond with 406 "Not Acceptable".
*
* The `type` value may be a single mime type string
* such as "application/json", the extension name
* such as "json" or an array `["json", "html", "text/plain"]`. When a list
* or array is given the _best_ match, if any is returned.
*
* Examples:
*
* // Accept: text/html
* this.accepts('html');
* // => "html"
*
* // Accept: text/*, application/json
* this.accepts('html');
* // => "html"
* this.accepts('text/html');
* // => "text/html"
* this.accepts('json', 'text');
* // => "json"
* this.accepts('application/json');
* // => "application/json"
*
* // Accept: text/*, application/json
* this.accepts('image/png');
* this.accepts('png');
* // => undefined
*
* // Accept: text/*;q=.5, application/json
* this.accepts(['html', 'json']);
* this.accepts('html', 'json');
* // => "json"
*/
accepts(): string[];
accepts(...types: string[]): string | false;
accepts(types: string[]): string | false;
/**
* Return accepted encodings or best fit based on `encodings`.
*
* Given `Accept-Encoding: gzip, deflate`
* an array sorted by quality is returned:
*
* ['gzip', 'deflate']
*/
acceptsEncodings(): string[];
acceptsEncodings(...encodings: string[]): string | false;
acceptsEncodings(encodings: string[]): string | false;
/**
* Return accepted charsets or best fit based on `charsets`.
*
* Given `Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5`
* an array sorted by quality is returned:
*
* ['utf-8', 'utf-7', 'iso-8859-1']
*/
acceptsCharsets(): string[];
acceptsCharsets(...charsets: string[]): string | false;
acceptsCharsets(charsets: string[]): string | false;
/**
* Return accepted languages or best fit based on `langs`.
*
* Given `Accept-Language: en;q=0.8, es, pt`
* an array sorted by quality is returned:
*
* ['es', 'pt', 'en']
*/
acceptsLanguages(): string[];
acceptsLanguages(...langs: string[]): string | false;
acceptsLanguages(langs: string[]): string | false;
/**
* Check if the incoming request contains the "Content-Type"
* header field, and it contains any of the give mime `type`s.
* If there is no request body, `null` is returned.
* If there is no content type, `false` is returned.
* Otherwise, it returns the first `type` that matches.
*
* Examples:
*
* // With Content-Type: text/html; charset=utf-8
* this.is('html'); // => 'html'
* this.is('text/html'); // => 'text/html'
* this.is('text/*', 'application/json'); // => 'text/html'
*
* // When Content-Type is application/json
* this.is('json', 'urlencoded'); // => 'json'
* this.is('application/json'); // => 'application/json'
* this.is('html', 'application/*'); // => 'application/json'
*
* this.is('html'); // => false
*/
// is(): string | boolean;
is(...types: string[]): string | false | null;
is(types: string[]): string | false | null;
/**
* Return request header. If the header is not set, will return an empty
* string.
*
* The `Referrer` header field is special-cased, both `Referrer` and
* `Referer` are interchangeable.
*
* Examples:
*
* this.get('Content-Type');
* // => "text/plain"
*
* this.get('content-type');
* // => "text/plain"
*
* this.get('Something');
* // => ''
*/
get(field: string): string;
}
declare interface ContextDelegatedResponse {
/**
* Get/Set response status code.
*/
status: number;
/**
* Get response status message
*/
message: string;
/**
* Get/Set response body.
*/
body: unknown;
/**
* Return parsed response Content-Length when present.
* Set Content-Length field to `n`.
*/
length: number;
/**
* Check if a header has been written to the socket.
*/
headerSent: boolean;
/**
* Vary on `field`.
*/
vary(field: string | string[]): void;
/**
* Perform a special-cased "back" to provide Referrer support.
* When Referrer is not present, `alt` or "/" is used.
*
* Examples:
*
* ctx.back()
* ctx.back('/index.html')
*/
back(alt?: string): void;
/**
* Perform a 302 redirect to `url`.
*
* The string "back" is special-cased
* to provide Referrer support, when Referrer
* is not present `alt` or "/" is used.
*
* Examples:
*
* this.redirect('/login');
* this.redirect('http://google.com');
*/
redirect(url: string): void;
/**
* Set Content-Disposition to "attachment" to signal the client to prompt for download.
* Optionally specify the filename of the download and some options.
*/
attachment(filename?: string, options?: contentDisposition.Options): void;
/**
* Return the response mime type void of
* parameters such as "charset".
*
* Set Content-Type response header with `type` through `mime.lookup()`
* when it does not contain a charset.
*
* Examples:
*
* this.type = '.html';
* this.type = 'html';
* this.type = 'json';
* this.type = 'application/json';
* this.type = 'png';
*/
type: string;
/**
* Get the Last-Modified date in Date form, if it exists.
* Set the Last-Modified date using a string or a Date.
*
* this.response.lastModified = new Date();
* this.response.lastModified = '2013-09-13';
*/
lastModified: Date;
/**
* Get/Set the ETag of a response.
* This will normalize the quotes if necessary.
*
* this.response.etag = 'md5hashsum';
* this.response.etag = '"md5hashsum"';
* this.response.etag = 'W/"123456789"';
*
* @param {String} etag
* @api public
*/
etag: string;
/**
* Set header `field` to `val`, or pass
* an object of header fields.
*
* Examples:
*
* this.set('Foo', ['bar', 'baz']);
* this.set('Accept', 'application/json');
* this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
*/
set(field: {
[key: string]: string | string[];
}): void;
set(field: string, val: string | string[]): void;
/**
* Append additional header `field` with value `val`.
*
* Examples:
*
* ```
* this.append('Link', ['', '']);
* this.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
* this.append('Warning', '199 Miscellaneous warning');
* ```
*/
append(field: string, val: string | string[]): void;
/**
* Remove header `field`.
*/
remove(field: string): void;
/**
* Checks if the request is writable.
* Tests for the existence of the socket
* as node sometimes does not set it.
*/
writable: boolean;
/**
* Flush any set headers, and begin the body
*/
flushHeaders(): void;
}
declare class Application extends EventEmitter {
proxy: boolean;
proxyIpHeader: string;
maxIpsCount: number;
middleware: Array>;
subdomainOffset: number;
env: string;
context: Application.BaseContext & ContextT;
request: Application.BaseRequest;
response: Application.BaseResponse;
silent: boolean;
keys: Keygrip | string[];
ctxStorage: AsyncLocalStorage$1 | undefined;
/**
* @param {object} [options] Application options
* @param {string} [options.env='development'] Environment
* @param {string[]} [options.keys] Signed cookie keys
* @param {boolean} [options.proxy] Trust proxy headers
* @param {number} [options.subdomainOffset] Subdomain offset
* @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
* @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity)
* @param {boolean|AsyncLocalStorage} [options.asyncLocalStorage] Pass `true` or an instance of `AsyncLocalStorage` to enable async local storage
*/
constructor(options?: {
env?: string | undefined;
keys?: string[] | undefined;
proxy?: boolean | undefined;
subdomainOffset?: number | undefined;
proxyIpHeader?: string | undefined;
maxIpsCount?: number | undefined;
asyncLocalStorage?: boolean | AsyncLocalStorage$1 | undefined;
});
/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*/
listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): Server;
listen(port: number, hostname?: string, listeningListener?: () => void): Server;
listen(port: number, backlog?: number, listeningListener?: () => void): Server;
listen(port: number, listeningListener?: () => void): Server;
listen(path: string, backlog?: number, listeningListener?: () => void): Server;
listen(path: string, listeningListener?: () => void): Server;
listen(options: ListenOptions, listeningListener?: () => void): Server;
listen(handle: any, backlog?: number, listeningListener?: () => void): Server;
listen(handle: any, listeningListener?: () => void): Server;
/**
* Return JSON representation.
* We only bother showing settings.
*/
inspect(): any;
/**
* Return JSON representation.
* We only bother showing settings.
*/
toJSON(): any;
/**
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*/
use(middleware: Application.Middleware): Application;
/**
* Return a request handler callback
* for node's native http/http2 server.
*/
callback(): (req: IncomingMessage | Http2ServerRequest, res: ServerResponse | Http2ServerResponse) => Promise;
/**
* Initialize a new context.
*
* @api private
*/
createContext(req: IncomingMessage, res: ServerResponse): Application.ParameterizedContext;
/**
* Default error handler.
*
* @api private
*/
onerror(err: Error): void;
/**
* return current context from async local storage
*/
readonly currentContext: ContextT | undefined;
}
declare namespace Application {
interface DefaultContextDelegatedRequest extends ContextDelegatedRequest {}
interface DefaultContextDelegatedResponse extends ContextDelegatedResponse {}
type DefaultStateExtends = any;
/**
* This interface can be augmented by users to add types to Koa's default state
*/
interface DefaultState extends DefaultStateExtends {}
type DefaultContextExtends = {};
/**
* This interface can be augmented by users to add types to Koa's default context
*/
interface DefaultContext extends DefaultContextExtends {
/**
* Custom properties.
*/
[key: PropertyKey]: any;
}
type Middleware = compose.Middleware>;
interface BaseRequest extends DefaultContextDelegatedRequest {
/**
* Get the charset when present or undefined.
*/
charset: string;
/**
* Return parsed Content-Length when present.
*/
length: number;
/**
* Return the request mime type void of
* parameters such as "charset".
*/
type: string;
/**
* Inspect implementation.
*/
inspect(): any;
/**
* Return JSON representation.
*/
toJSON(): any;
}
interface BaseResponse extends DefaultContextDelegatedResponse {
/**
* Return the request socket.
*
* @return {Connection}
* @api public
*/
socket: Socket;
/**
* Return response header.
*/
header: OutgoingHttpHeaders;
/**
* Return response header, alias as response.header
*/
headers: OutgoingHttpHeaders;
/**
* Check whether the response is one of the listed types.
* Pretty much the same as `this.request.is()`.
*
* @param {String|Array} types...
* @return {String|false}
* @api public
*/
// is(): string;
is(...types: string[]): string | false | null;
is(types: string[]): string | false | null;
/**
* Return response header. If the header is not set, will return an empty
* string.
*
* The `Referrer` header field is special-cased, both `Referrer` and
* `Referer` are interchangeable.
*
* Examples:
*
* this.get('Content-Type');
* // => "text/plain"
*
* this.get('content-type');
* // => "text/plain"
*
* this.get('Something');
* // => ''
*/
get(field: string): string;
/**
* Inspect implementation.
*/
inspect(): any;
/**
* Return JSON representation.
*/
toJSON(): any;
}
interface BaseContext extends DefaultContextDelegatedRequest, DefaultContextDelegatedResponse {
/**
* util.inspect() implementation, which
* just returns the JSON output.
*/
inspect(): any;
/**
* Return JSON representation.
*
* Here we explicitly invoke .toJSON() on each
* object, as iteration will otherwise fail due
* to the getters and cause utilities such as
* clone() to fail.
*/
toJSON(): any;
/**
* Similar to .throw(), adds assertion.
*
* this.assert(this.user, 401, 'Please login!');
*
* See: https://github.com/jshttp/http-assert
*/
assert: typeof httpAssert;
/**
* Throw an error with `msg` and optional `status`
* defaulting to 500. Note that these are user-level
* errors, and the message may be exposed to the client.
*
* this.throw(403)
* this.throw('name required', 400)
* this.throw(400, 'name required')
* this.throw('something exploded')
* this.throw(new Error('invalid'), 400);
* this.throw(400, new Error('invalid'));
*
* See: https://github.com/jshttp/http-errors
*/
throw(status: number, ...args: HttpErrors.UnknownError[]): never;
throw(...args: HttpErrors.UnknownError[]): never;
/**
* Default error handling.
*/
onerror(err: Error): void;
}
interface Request extends BaseRequest {
app: Application;
req: IncomingMessage;
res: ServerResponse;
ctx: Context;
response: Response;
originalUrl: string;
ip: string;
accept: accepts.Accepts;
}
interface Response extends BaseResponse {
app: Application;
req: IncomingMessage;
res: ServerResponse;
ctx: Context;
request: Request;
}
interface ExtendableContext extends BaseContext {
app: Application;
request: Request;
response: Response;
req: IncomingMessage;
res: ServerResponse;
originalUrl: string;
cookies: Cookies;
accept: accepts.Accepts;
/**
* To bypass Koa's built-in response handling, you may explicitly set `ctx.respond = false;`
*/
respond?: boolean | undefined;
}
type ParameterizedContext = ExtendableContext & {
state: StateT;
} & ContextT & {
body: ResponseBodyT;
response: {
body: ResponseBodyT;
};
};
interface Context extends ParameterizedContext {}
type Next = () => Promise;
/**
* A re-export of `HttpError` from the `http-error` package.
*
* This is the error type that is thrown by `ctx.assert()` and `ctx.throw()`.
*/
const HttpError: typeof HttpErrors.HttpError;
}
//#endregion
//#region src/context.d.ts
/**
* Returns the verified JWT payload for the current MCP request.
* Only available inside a tool handler when `auth` is configured on the router.
*
* Accepts an optional type parameter to avoid casting at call sites:
* `getIdentity<{ sub: string; email: string }>()` returns `T | undefined`.
* Omitting the type parameter keeps the return type as `unknown | undefined`.
*/
declare const getIdentity: () => T | undefined;
//#endregion
//#region src/registerToolFromSchema.d.ts
/**
* A plain JSON Schema object (draft-07 compatible) describing the shape of a
* tool's input. Used with {@link registerToolFromSchema} as an alternative to
* providing a Zod shape, enabling lossless round-trips for schemas that contain
* features not expressible in Zod v3 (`anyOf`, `$ref`, `pattern`, `allOf`, …).
*/
interface JsonObjectSchema {
type: 'object';
properties?: Record;
required?: string[];
[key: string]: unknown;
}
/**
* Parameters accepted by {@link registerToolFromSchema}.
*/
interface RegisterToolFromSchemaParams {
/** Unique tool name. */
name: string;
/** Human-readable description shown to the AI client. */
description?: string;
/**
* Plain JSON Schema that describes the tool's input object.
* This schema is forwarded verbatim over the MCP wire protocol, so any
* JSON Schema feature (`anyOf`, `$ref`, `pattern`, …) is preserved without
* loss. Defaults to `{ type: 'object', properties: {} }` when omitted.
*/
inputSchema?: JsonObjectSchema;
/**
* Whether `tools/call` arguments are validated against `inputSchema` before
* the handler runs, rejecting a mismatch with an MCP error.
*
* Defaults to `false`, which forwards arguments to the handler unchecked —
* the behavior this helper has always had. `inputSchema` is still advertised
* verbatim over `tools/list` either way; this only controls enforcement.
*
* Enable it once you know `inputSchema` describes every value the tool
* genuinely accepts. Schemas generated from an OpenAPI document are a common
* source of *incomplete* ones — a field a client may send as `null` to clear
* it, or one accepting several shapes, is easy to emit as a bare
* `{ type: 'string' }`. Validating against a schema like that rejects calls
* the underlying API would have accepted.
*
* @default false
*/
validateArguments?: boolean;
/**
* Tool handler invoked when the AI client calls the tool.
* Receives the request arguments, validated against `inputSchema` only when
* `validateArguments` is enabled.
*/
handler: (args: Record) => CallToolResult | Promise;
}
/**
* Registers a tool on an MCP server using a **plain JSON Schema** object for
* `inputSchema` instead of a Zod shape.
*
* This is useful when tool definitions are shared between the MCP server and
* an AI SDK agent (e.g. Vercel AI SDK's `tool()` helper), because both consume
* a plain JSON Schema at runtime. Using this helper eliminates the lossy
* JSON-Schema→Zod conversion that would otherwise be required.
*
* Thin wrapper over `@modelcontextprotocol/server`'s `fromJsonSchema`, which
* converts a JSON Schema into a Standard Schema that `registerTool` accepts
* directly, so the schema round-trips verbatim over `tools/list`. Arguments
* reach the handler unchecked unless `validateArguments` is enabled.
*
* @param server - The `McpServer` instance to register the tool on.
* @param params - Tool configuration including name, description, inputSchema,
* validateArguments, and handler.
*
* @example
* ```typescript
* import { registerToolFromSchema, McpServer } from '@ttoss/http-server-mcp';
*
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
*
* registerToolFromSchema(server, {
* name: 'get-project',
* description: 'Get a project by ID',
* inputSchema: {
* type: 'object',
* properties: { id: { type: 'string', description: 'Project public ID' } },
* required: ['id'],
* },
* handler: async ({ id }) => ({
* content: [{ type: 'text', text: `Project: ${id}` }],
* }),
* });
* ```
*/
declare const registerToolFromSchema: (server: McpServer$1, params: RegisterToolFromSchemaParams) => void;
//#endregion
//#region src/createGatedToolRegistrar.d.ts
/** Resolved identity for a gated tool call. */
type ToolIdentity = {
userId: string;
scopes?: string[];
};
/**
* Full context passed to gates, `buildContext`, and `onError` on every tool
* invocation. Having all three fields in one object means gate callbacks can
* be both identity-aware and args-aware without multiple parameters.
*/
type ToolCallContext = {
/** The resolved caller identity. */identity: ToolIdentity;
/**
* The validated tool input (post SDK parse). Present for gates and
* `buildContext`. Arg-conditional gates read this to vary their predicate
* per call.
*/
args: Record; /** Tool name, for error attribution and gate labelling. */
handler: string;
};
/** Definition for a single gated tool. */
type GatedToolDef = {
/** Tool name as registered with the MCP server. */name: string; /** Human-readable tool description. */
description: string; /** The single scope that must be present on the caller's token. */
requiredScope: string; /** Zod field map or ZodObject — passed through to `server.registerTool`. */
inputSchema: unknown;
/**
* Per-tool gates merged after the global `gates`. Useful for arg-conditional
* authorization (e.g. different subscription tiers based on call args).
* Each gate receives the full {@link ToolCallContext} (identity + args).
* Throw to reject the call; return (or resolve) to continue.
*/
gates?: Array<(ctx: ToolCallContext) => void | Promise>; /** The tool handler. Receives merged call args + `buildContext` output. */
method: (args: Record) => Promise;
};
/** Options for {@link createGatedToolRegistrar}. */
type CreateGatedToolRegistrarOptions = {
/** The MCP server instance to register tools on. */server: McpServer$1;
/**
* Called once per tool invocation to resolve the caller's identity.
* Defaults to `getIdentity()` from the request context.
*/
resolveIdentity?: () => ToolIdentity;
/**
* Global authorization gates run after the scope check, in order, before
* per-tool gates. Each receives the full {@link ToolCallContext} — both
* identity and the validated call args — so gate predicates may be
* conditional on either.
* Throw to reject the call; return (or resolve) to continue.
* Gates own their own error handling — `onError` covers the tool handler only.
*/
gates?: Array<(ctx: ToolCallContext) => void | Promise>;
/**
* When `true` (default), checks `def.requiredScope` against `identity.scopes`
* and returns an `isError` result when the scope is absent. Set to `false`
* to skip the scope check (e.g. when all scopes are enforced by `gates`).
*/
enforceScope?: boolean;
/**
* Called when the tool **handler** throws. Use for error reporting/telemetry.
* The error is always rethrown after this hook completes.
* Note: scope-check failures and gate rejections do not trigger `onError`.
*/
onError?: (error: unknown, ctx: ToolCallContext) => void | Promise;
/**
* Called once per invocation to produce extra key-value pairs that are
* merged into the handler args. Receives the full {@link ToolCallContext}
* so context can vary by identity or by call args.
*/
buildContext?: (ctx: ToolCallContext) => Record;
/**
* Message returned as an `isError` result when the handler resolves to
* `null` or `undefined`. Defaults to `"Not found"`.
*/
notFoundMessage?: string;
};
declare const createGatedToolRegistrar: ({
server,
resolveIdentity,
gates,
enforceScope,
onError,
buildContext,
notFoundMessage
}: CreateGatedToolRegistrarOptions) => {
register: (def: GatedToolDef) => void;
};
//#endregion
//#region src/index.d.ts
type Context$1 = Application.Context;
/** Amazon Cognito user pool configuration for JWT verification. */
interface CognitoUserPoolConfig {
/** The Cognito User Pool ID (e.g. `us-east-1_abc123`). */
userPoolId: string;
/**
* Which token type to verify.
* @default 'access'
*/
tokenUse?: 'access' | 'id';
/** The app client ID registered in the User Pool. */
clientId: string;
}
/**
* Authentication options for the MCP endpoint. Verification runs through
* `@ttoss/http-server-auth`'s `oauth` strategy; supply either a Cognito user
* pool or a custom `verifyToken`.
*/
interface McpAuthOptions {
/** Amazon Cognito user pool config; a `CognitoJwtVerifier` is built from it. */
cognitoUserPool?: CognitoUserPoolConfig;
/**
* Custom token verifier for non-Cognito providers (Auth0, Keycloak, your own
* JWTs, opaque tokens). Resolve with the verified payload, or throw to reject.
*/
verifyToken?: (token: string) => Promise;
/**
* Scopes that must all be present on the token, else `403`.
* `verifyToken` may return either `scope: string` (space-separated) or
* `scopes: string[]`; both are normalised internally.
*/
requiredScopes?: string[];
/**
* JSON-RPC methods (read from `body.method`) that bypass verification.
* Leaving this unset serves `tools/list` — the full tool catalogue —
* to unauthenticated callers, and logs a one-time warning explaining how
* to close it. Set explicitly (even to the same default) to silence the
* warning; set to `['initialize']` to require a token for `tools/list` too.
* @default ['initialize', 'tools/list']
*/
publicMethods?: string[];
/**
* When set, a `401` carries `WWW-Authenticate: Bearer resource_metadata="…"`
* (RFC 9728) so MCP clients can discover the authorization server.
*/
resourceMetadataUrl?: string;
/**
* URL of this MCP server, surfaced in the OAuth Protected Resource Metadata
* response. Both this and `authorizationServerUrl` must be set to serve
* `/.well-known/oauth-protected-resource`.
*/
resourceServerUrl?: string;
/** URL of the OAuth Authorization Server that issues tokens for this resource. */
authorizationServerUrl?: string;
/**
* Expected audience — the resource indicator (RFC 8707) this MCP server
* identifies as. When set, the verified token's `aud` claim must include at
* least one of these values, or the request is rejected with `401`. Without
* this check, a token minted for a *different* resource but signed by the
* same authorization server would still be accepted here — the classic
* confused-deputy risk RFC 8707 exists to close.
*
* Applies uniformly regardless of whether verification is done via
* `cognitoUserPool`, a custom `verifyToken`, or `@ttoss/auth-core/oidc`'s
* `createOidcVerifier` (which intentionally leaves audience validation to
* the caller for this reason).
*
* @example 'https://mcp.example.com'
*/
resourceIndicator?: string | string[];
}
/**
* Options for a single `apiCall` request.
*/
interface ApiCallOptions {
/**
* JSON-serialisable request body. Automatically serialised and sent with
* `Content-Type: application/json`.
*/
body?: unknown;
/**
* Additional or override headers for this specific request.
* These are merged on top of any headers injected from the MCP request
* context via `getApiHeaders`, allowing per-call overrides.
*/
headers?: Record;
}
/**
* Generic HTTP helper for use inside MCP tool handlers.
*
* Accepts any full URL (third-party APIs, public APIs, etc.) or a path
* relative to the `apiBaseUrl` configured in `createMcpRouter`.
*
* Headers configured via `getApiHeaders` in `createMcpRouter` are injected
* automatically into every request, allowing transparent forwarding of auth
* tokens, API keys, or any other header — without coupling this helper to a
* specific authentication scheme. Per-call `options.headers` take precedence
* over context-injected headers.
*
* @param method - HTTP method (e.g. `'GET'`, `'POST'`, `'PUT'`, `'DELETE'`)
* @param url - Full URL **or** a path starting with `/` (appended to `apiBaseUrl`)
* @param options - Optional body and per-call header overrides
* @returns Parsed JSON response body
*
* @example Bearer token forwarding (configured once in `createMcpRouter`)
* ```typescript
* import { apiCall, createMcpRouter, McpServer } from '@ttoss/http-server-mcp';
*
* // Tool handler – no manual auth wiring needed
* mcpServer.registerTool('list-portfolios', { description: '...', inputSchema: {} }, async () => {
* const data = await apiCall('GET', '/portfolios');
* return { content: [{ type: 'text', text: JSON.stringify(data) }] };
* });
*
* const mcpRouter = createMcpRouter(mcpServer, {
* apiBaseUrl: `http://localhost:${process.env.PORT}/api/v1`,
* // Forward the caller's Bearer token to every apiCall
* getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' }),
* });
* ```
*
* @example x-api-key forwarding
* ```typescript
* const mcpRouter = createMcpRouter(mcpServer, {
* apiBaseUrl: 'https://internal-service/api',
* getApiHeaders: (ctx) => ({
* 'x-api-key': ctx.headers['x-api-key'] as string,
* }),
* });
* ```
*
* @example Third-party or public API (full URL, no context required)
* ```typescript
* const weather = await apiCall('GET', 'https://api.weather.com/current?city=Berlin');
* const created = await apiCall('POST', 'https://api.example.com/items', {
* body: { name: 'widget' },
* headers: { Authorization: 'Bearer fixed-service-token' },
* });
* ```
*/
declare const apiCall: (method: string, url: string, options?: ApiCallOptions) => Promise;
/**
* Asserts that the current request's token contains all required scopes.
* Throws if any scope is missing — the MCP SDK catches this and returns a
* tool error to the client. Use inside tool handlers for per-tool authorization.
*
* Accepts either `scope: string` (space-separated, standard JWT claim) or
* `scopes: string[]` from `verifyToken`. If neither is present and `required`
* is non-empty, throws with a descriptive message instead of a silent 403.
*
* @example
* ```typescript
* server.tool('delete-user', schema, async (args) => {
* checkScopes(['admin', 'write:users']);
* // proceed only if caller has both scopes
* });
* ```
*/
declare const checkScopes: (required: string[]) => void;
/**
* Options for configuring the MCP router
*/
interface McpRouterOptions {
/**
* The HTTP path where the MCP server will be mounted
* @default '/mcp'
*/
path?: string;
/**
* Additional HTTP paths where the MCP server is also mounted.
*
* Useful when MCP clients differ in where they connect after OAuth: some
* follow the protected-resource `resource` metadata value as the endpoint,
* others always connect to the bare origin (`/`). Setting `aliases: ['/']`
* serves both without requiring app-level path rewrites.
*
* @example ['/'] // also handle MCP requests at the bare root
*/
aliases?: string[];
/**
* Optional session ID generator for stateful MCP servers.
* When provided, a single shared transport is created and sessions are tracked.
* When undefined (default), the server operates in stateless mode where each
* HTTP request uses its own transport instance.
*
* Applies to 2025-era traffic. The `2026-07-28` protocol revision has no
* session concept in its core, so requests speaking that revision are always
* served statelessly regardless of this option.
*/
sessionIdGenerator?: () => string;
/**
* Base URL prepended to relative paths passed to `apiCall` (paths starting
* with `/`). Tool handlers can then call `apiCall('GET', '/resource')` without
* specifying a host.
*
* @example 'http://localhost:3000/api/v1'
*/
apiBaseUrl?: string;
/**
* Called once per incoming MCP HTTP request. Return a plain object whose
* key-value pairs will be merged into the headers of every `apiCall` made
* within that request's tool handlers.
*
* Use this to forward any header from the MCP request — Bearer tokens, API
* keys, tenant IDs, trace headers, etc. — without coupling tool handlers to
* a specific authentication scheme.
*
* @example Forward a Bearer token
* ```typescript
* getApiHeaders: (ctx) => ({ Authorization: ctx.headers.authorization ?? '' })
* ```
*
* @example Forward an x-api-key header
* ```typescript
* getApiHeaders: (ctx) => ({ 'x-api-key': ctx.headers['x-api-key'] as string })
* ```
*
* @example Inject a static service-to-service key
* ```typescript
* getApiHeaders: () => ({ 'x-internal-key': process.env.INTERNAL_API_KEY! })
* ```
*/
getApiHeaders?: (ctx: Context$1) => Record;
/**
* OAuth / JWT authentication configuration for the MCP endpoint.
*
* When set, incoming MCP requests must include a valid Bearer token in the
* `Authorization` header — except for `publicMethods` (by default
* `initialize` and `tools/list`), which bypass verification so clients can
* discover the server before authenticating. Invalid or missing tokens
* receive a `401` response with `WWW-Authenticate: Bearer` (or
* `Bearer resource_metadata="..."` when `resourceMetadataUrl` is set, per
* RFC 9728). Tokens that fail a `requiredScopes` check receive `403`.
*
* The verified token payload is accessible inside tool handlers via
* {@link getIdentity}. Fine-grained per-tool scope checks can be done with
* {@link checkScopes}.
*
* @example Cognito
* ```typescript
* createMcpRouter(server, {
* auth: {
* cognitoUserPool: { userPoolId: 'us-east-1_xxx', clientId: 'yyy' },
* requiredScopes: ['mcp:access'],
* },
* });
* ```
*
* @example Custom verifier
* ```typescript
* createMcpRouter(server, {
* auth: {
* verifyToken: async (token) => myJwtLib.verify(token),
* },
* });
* ```
*/
auth?: McpAuthOptions;
}
/**
* Creates a Koa router configured to handle MCP protocol requests
*
* @param server - The MCP server instance with registered tools and resources
* @param options - Configuration options for the router
* @returns A Koa Router instance configured for MCP
*
* @example
* ```typescript
* import { App, bodyParser } from '@ttoss/http-server';
* import { createMcpRouter, McpServer, z } from '@ttoss/http-server-mcp';
*
* const mcpServer = new McpServer({
* name: 'my-server',
* version: '1.0.0',
* });
*
* mcpServer.registerTool(
* 'hello',
* {
* description: 'Say hello',
* inputSchema: { name: z.string() },
* },
* async ({ name }) => ({
* content: [{ type: 'text', text: `Hello, ${name}!` }],
* })
* );
*
* const app = new App();
* app.use(bodyParser());
*
* const mcpRouter = createMcpRouter(mcpServer);
* app.use(mcpRouter.routes());
*
* app.listen(3000);
* ```
*/
declare const createMcpRouter: (server: McpServer$1, options?: McpRouterOptions) => Router;
//#endregion
export { ApiCallOptions, CognitoUserPoolConfig, type CreateGatedToolRegistrarOptions, type GatedToolDef, type JsonObjectSchema, McpAuthOptions, McpRouterOptions, McpServer, type RegisterToolFromSchemaParams, type ToolCallContext, type ToolIdentity, apiCall, checkScopes, createGatedToolRegistrar, createMcpRouter, getIdentity, registerToolFromSchema, z };