Files
TREK/nest-mcp/src/types.ts
T

178 lines
6.6 KiB
TypeScript

import type { ZodRawShape } from 'zod';
/**
* The per-session context handed to `McpRegistry.attach(server, ctx)` and
* forwarded to every handler (last argument) and access predicate/policy.
*
* The package defines NO context fields of its own — the host application
* augments this interface once:
*
* declare module '@trek/nest-mcp' {
* interface McpContext { userId: number; scopes: string[] | null }
* }
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface McpContext {}
/**
* Host-augmentable registry of valid `access.group` values — the `McpContext`
* augmentation trick applied to groups. Augment once:
*
* declare module '@trek/nest-mcp' {
* interface McpAccessGroupRegistry extends Record<MyGroupUnion, true> {}
* }
*
* Unaugmented, `McpAccessGroup` stays `string` — the package attaches no
* semantics to groups, same contract as `McpContext`.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface McpAccessGroupRegistry {}
export type McpAccessGroup = keyof McpAccessGroupRegistry extends never
? string
: keyof McpAccessGroupRegistry & string;
/**
* Host augmentation point for the access *mode*, mirroring
* `McpAccessGroupRegistry`:
*
* declare module '@trek/nest-mcp' {
* interface McpAccessModeRegistry extends Record<MyModeUnion, true> {}
* }
*
* Unaugmented, `McpAccessMode` falls back to `'read' | 'write'` — the two
* modes every host had before this was augmentable, so leaving the registry
* empty keeps the previous contract exactly.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface McpAccessModeRegistry {}
export type McpAccessMode = keyof McpAccessModeRegistry extends never
? 'read' | 'write'
: keyof McpAccessModeRegistry & string;
/**
* Declarative access marker resolved by the host-supplied `accessPolicy`.
* The package attaches no semantics to `group`/`mode` — the policy does.
*/
export interface McpDeclarativeAccess {
group: McpAccessGroup;
mode: McpAccessMode;
}
/** Predicate escape hatch — bypasses the policy entirely. */
export type McpAccessPredicate = (ctx: McpContext) => boolean;
export type McpAccess = McpDeclarativeAccess | McpAccessPredicate;
/**
* Host-supplied resolver for declarative access, given once at
* `McpModule.forRoot({ accessPolicy })`. Return true to register the entry
* on the session's server.
*/
export type McpAccessPolicy = (access: McpDeclarativeAccess, ctx: McpContext) => boolean;
interface McpEntryOptionsBase {
name: string;
title?: string;
description?: string;
/**
* Availability gate evaluated BEFORE `access` (short-circuits it) — for
* "is this feature on at all" checks like addon toggles. Keeps `access`
* purely about permissions, so declarative scope markers still compose
* with runtime feature gates instead of being replaced by predicates.
*
* `self` is the `@McpController()` instance the entry was declared on,
* handed in at attach time. Without it the gate could only reach a
* module-level singleton, because the options object is built when the class
* is defined and there is no `this` yet — which is how a host ends up
* constructing a second copy of a service outside its own container just to
* answer "is this addon on". With it, the predicate reads an injected
* collaborator and the dependency is visible in the module graph.
*
* Declared with method syntax on purpose: that makes the parameter bivariant,
* so a predicate written against its own controller class stays assignable
* here. The registry always passes the instance that declared the entry.
*/
when?(ctx: McpContext, self: object): boolean;
/** Omitted ⇒ the entry is always registered (subject to `when`). */
access?: McpAccess;
}
/**
* Mirrors the SDK's `registerTool` config. `inputSchema` is a ZodRawShape
* passed straight through — the SDK does the parsing/validation.
* `annotations`/`_meta` are kept structural so the package's public d.ts
* never imports deep SDK subpaths.
*/
export interface ToolOptions extends McpEntryOptionsBase {
inputSchema?: ZodRawShape;
outputSchema?: ZodRawShape;
annotations?: Record<string, unknown>;
_meta?: Record<string, unknown>;
}
/** Mirrors `registerResource(name, uri, metadata, cb)` for a fixed URI. */
export interface ResourceOptions extends McpEntryOptionsBase {
uri: string;
mimeType?: string;
_meta?: Record<string, unknown>;
}
/** Mirrors `registerResource(name, new ResourceTemplate(uriTemplate), metadata, cb)`. */
export interface ResourceTemplateOptions extends McpEntryOptionsBase {
uriTemplate: string;
mimeType?: string;
_meta?: Record<string, unknown>;
}
/** Mirrors the SDK's `registerPrompt` config (argsSchema entries must be string-valued Zod schemas). */
export interface PromptOptions extends McpEntryOptionsBase {
argsSchema?: ZodRawShape;
}
export type McpEntryKind = 'tool' | 'resource' | 'resourceTemplate' | 'prompt';
/** Per-session options for `McpRegistry.attach()`. */
export interface McpAttachOptions {
/**
* Called immediately before every attached handler runs, with the entry's
* kind and registered name. This is the host's observability seam (e.g. a
* tool-call audit trail) — nest-mcp itself attaches no semantics to it.
* Contract: the hook MUST NOT throw; nest-mcp does not catch, so a throwing
* hook fails the very invocation it is observing. Hosts wrap their own
* failure handling.
*/
onInvoke?: (info: { kind: McpEntryKind; name: string }) => void;
}
export type McpEntry =
| { kind: 'tool'; methodName: string; options: ToolOptions }
| { kind: 'resource'; methodName: string; options: ResourceOptions }
| { kind: 'resourceTemplate'; methodName: string; options: ResourceTemplateOptions }
| { kind: 'prompt'; methodName: string; options: PromptOptions };
/** Introspection view of one recorded entry (see `McpRegistry.list()`). */
export interface McpRegistryListing {
kind: McpEntryKind;
name: string;
className: string;
methodName: string;
access?: McpAccess;
}
/**
* Host-supplied per-entry check run by `validate()`. Return a problem
* description to fail boot, null/undefined to accept. Only called for
* entries with DECLARATIVE access (predicates are opaque to the host).
*/
export type McpAccessValidator = (access: McpDeclarativeAccess, entry: McpRegistryListing) => string | null | undefined;
export interface McpModuleOptions {
accessPolicy?: McpAccessPolicy;
validateAccess?: McpAccessValidator;
}
/** Injection token for the options object given to `McpModule.forRoot()`. */
export const MCP_MODULE_OPTIONS = Symbol('MCP_MODULE_OPTIONS');