@voyantjs/catalog-mcp 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @voyantjs/catalog-mcp
2
+
3
+ Phase 2.x — MCP (Model Context Protocol) server scaffolding for the catalog
4
+ plane. Wraps the catalog plane's APIs as agent-callable tools so AI assistants
5
+ (Claude, ChatGPT plugins, custom agents) connect with tenant-scoped credentials
6
+ and call tools rather than crafting REST calls.
7
+
8
+ See [`docs/architecture/catalog-rag-architecture.md`](../../docs/architecture/catalog-rag-architecture.md)
9
+ §3 + §12 (Open question 1).
10
+
11
+ ## Architectural commitment
12
+
13
+ > AI agents query the API, not the vector database directly.
14
+
15
+ The MCP tools wrap `getResolvedXById`, `executeSemanticSearch`,
16
+ `federateAudienceSearch`, and the source adapter live-resolve / quote paths —
17
+ **never the vector DB**. Visibility filtering, overlay resolution, audit, and
18
+ rate limiting all happen at the API layer where they normally do. The MCP
19
+ server is a thin transport.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pnpm add @voyantjs/catalog-mcp
25
+ ```
26
+
27
+ ## What's in the box
28
+
29
+ - **`./contract`** — `McpToolDefinition`, `McpToolHandler`, `McpToolContext`
30
+ types. Transport-agnostic: define the tool surface here, wire it into your
31
+ MCP transport of choice (the `@modelcontextprotocol/sdk`, your own HTTP
32
+ layer, an SSE handler, etc.).
33
+ - **`./registry`** — `createMcpToolRegistry` + `dispatchTool` helpers.
34
+ Templates register all tools at startup and the registry exposes a
35
+ unified dispatch entry point for the transport layer.
36
+ - **`./tools/*`** — Five canonical tools per the architecture doc:
37
+ - `search_catalog` — keyword / hybrid / semantic search across a vertical
38
+ - `get_entity` — fetch a single resolved CatalogEntry view
39
+ - `suggest_alternatives` — semantic similarity for "more like this"
40
+ - `check_availability` — calls the source adapter's volatile-live path
41
+ - `get_quote` — calls the source adapter to lock a price proposition
42
+
43
+ ## Tool isolation guarantees
44
+
45
+ - All tools enforce visibility filtering through the catalog plane's resolver.
46
+ A `customer`-actor agent never receives staff-only fields, regardless of
47
+ how cleverly the agent crafts the search query.
48
+ - `search_catalog` uses `executeSemanticSearch` — the agent's audience pool
49
+ is enforced server-side; cross-audience federation requires a `staff`-actor
50
+ context.
51
+ - `check_availability` and `get_quote` route through the source adapter —
52
+ volatile-live values are always live, never cached, never embedded.
53
+
54
+ ## Wiring into an MCP transport
55
+
56
+ Templates wire the registry into their MCP SDK transport of choice:
57
+
58
+ ```typescript
59
+ import { createMcpToolRegistry } from "@voyantjs/catalog-mcp/registry"
60
+ import { searchCatalogTool } from "@voyantjs/catalog-mcp/tools/search-catalog"
61
+ import { getEntityTool } from "@voyantjs/catalog-mcp/tools/get-entity"
62
+ // ... and so on
63
+
64
+ const registry = createMcpToolRegistry({
65
+ context: {
66
+ actor: "staff",
67
+ tenantId: "operator_xyz",
68
+ catalog: { /* injected services */ },
69
+ },
70
+ })
71
+
72
+ registry.register(searchCatalogTool)
73
+ registry.register(getEntityTool)
74
+ // ... wire registry.dispatchTool into your MCP server's CallTool handler.
75
+ ```
76
+
77
+ The `@modelcontextprotocol/sdk` is **not** a dependency of this package — the
78
+ catalog-mcp surface is the tool definitions; the transport is the operator's
79
+ choice (stdio for local dev, HTTP+SSE for hosted, custom for templates).
@@ -0,0 +1,158 @@
1
+ /**
2
+ * MCP tool contract — transport-agnostic shapes for catalog tools.
3
+ *
4
+ * Every catalog tool is defined as a `McpToolDefinition`: a name, a
5
+ * description (shown to the LLM), an input zod schema, and a handler that
6
+ * takes the parsed args + the per-request context and returns a structured
7
+ * result.
8
+ *
9
+ * Templates wire the registry into the actual MCP transport of their
10
+ * choice (the `@modelcontextprotocol/sdk` for stdio/HTTP+SSE, a custom
11
+ * Hono route, etc.). This package's surface ends at the tool definitions.
12
+ *
13
+ * See `docs/architecture/catalog-rag-architecture.md` §3 + §12.
14
+ */
15
+ import type { IndexerAdapter, IndexerSlice, ResolverScope, Visibility } from "@voyantjs/catalog";
16
+ import type { EmbeddingProvider } from "@voyantjs/catalog-rag";
17
+ import type { z } from "zod";
18
+ /**
19
+ * Per-request context passed to every tool handler.
20
+ *
21
+ * Templates construct this once per agent connection (typically derived
22
+ * from the agent's API key / OAuth token / session). The MCP transport
23
+ * passes the same context to every tool dispatch in that session.
24
+ */
25
+ export interface McpToolContext {
26
+ /** The actor identity. Drives visibility filtering at every layer. */
27
+ actor: Visibility;
28
+ /** Tenant / operator identifier — usually synthesized into provenance. */
29
+ tenantId: string;
30
+ /** Default scope for tools that need locale / audience / market. */
31
+ defaultScope: ResolverScope;
32
+ /** Catalog services / adapters injected by the template. */
33
+ catalog: McpCatalogServices;
34
+ }
35
+ /**
36
+ * The catalog plane services tools call into. Templates inject the
37
+ * concrete instances (e.g. the IndexerAdapter, the EmbeddingProvider, the
38
+ * per-vertical resolved-fetch helpers) at registry construction time.
39
+ *
40
+ * Each service is optional — tools that need a service it isn't given
41
+ * fail with a clear error rather than producing wrong results silently.
42
+ */
43
+ export interface McpCatalogServices {
44
+ /** Indexer for keyword / hybrid / semantic search. */
45
+ indexer?: IndexerAdapter;
46
+ /** Embedding provider — required for semantic / hybrid search. */
47
+ embeddings?: EmbeddingProvider;
48
+ /**
49
+ * Per-vertical resolved-fetch functions, keyed by vertical name. Each
50
+ * takes (entityId, scope) and returns the resolved view or null. Used
51
+ * by `get_entity` and `suggest_alternatives`.
52
+ */
53
+ resolveEntity?: (vertical: string, entityId: string, scope: ResolverScope) => Promise<McpResolvedEntity | null>;
54
+ /**
55
+ * Per-vertical live-availability function — calls the source adapter
56
+ * for volatile-live availability fields. Used by `check_availability`.
57
+ */
58
+ checkAvailability?: (vertical: string, entityId: string, parameters: Record<string, unknown>) => Promise<McpAvailabilityResult>;
59
+ /**
60
+ * Per-vertical live-quote function — calls the source adapter to lock
61
+ * a priced quote. Used by `get_quote`.
62
+ */
63
+ getQuote?: (vertical: string, entityId: string, parameters: Record<string, unknown>) => Promise<McpQuoteResult>;
64
+ /**
65
+ * Default slice resolver. Given a vertical + the context's defaultScope,
66
+ * returns the IndexerSlice the tool should query. Templates may override
67
+ * this to map their own audience taxonomy to slices.
68
+ */
69
+ defaultSliceFor?: (vertical: string, scope: ResolverScope) => IndexerSlice;
70
+ }
71
+ /** Resolved-view shape returned by `get_entity` and similar. */
72
+ export interface McpResolvedEntity {
73
+ vertical: string;
74
+ entityId: string;
75
+ /** Resolved field values (visibility-filtered for the actor). */
76
+ fields: Record<string, unknown>;
77
+ /** Sources / provenance — which slice satisfied each overlayed field. */
78
+ provenance?: Record<string, {
79
+ locale: string;
80
+ audience: string;
81
+ market: string;
82
+ } | null>;
83
+ }
84
+ /** Live-availability response shape — exact shape is vertical-dependent. */
85
+ export interface McpAvailabilityResult {
86
+ available: boolean;
87
+ /** Optional structured details (room counts, departure capacity, etc.). */
88
+ details?: Record<string, unknown>;
89
+ /** When the availability was checked — useful for UX timestamps. */
90
+ checkedAt: string;
91
+ }
92
+ /** Live-quote response shape. */
93
+ export interface McpQuoteResult {
94
+ quoteId: string;
95
+ totalPrice: {
96
+ amount: string;
97
+ currency: string;
98
+ };
99
+ /** When the quote expires — agents should warn users about this. */
100
+ expiresAt?: string;
101
+ /** Optional structured breakdown — base / taxes / fees / surcharges. */
102
+ breakdown?: Record<string, unknown>;
103
+ }
104
+ /**
105
+ * MCP tool definition. The name + description are surfaced to the LLM;
106
+ * `inputSchema` validates and types the args; `handler` is the actual
107
+ * implementation.
108
+ *
109
+ * MCP standardly uses JSON Schema for inputSchema; we use Zod here and
110
+ * the registry helper exports a `toJsonSchema` adapter consumers wire
111
+ * into their MCP transport.
112
+ */
113
+ export interface McpToolDefinition<TArgs = unknown, TResult = McpToolResult> {
114
+ /** Tool name, surfaced to the LLM. Convention: snake_case. */
115
+ name: string;
116
+ /** Human-readable description shown to the LLM. */
117
+ description: string;
118
+ /** Zod schema for the args; the dispatcher parses + validates. */
119
+ inputSchema: z.ZodType<TArgs>;
120
+ /** Handler — receives parsed args + context, returns the result. */
121
+ handler: McpToolHandler<TArgs, TResult>;
122
+ }
123
+ export type McpToolHandler<TArgs, TResult> = (args: TArgs, context: McpToolContext) => Promise<TResult>;
124
+ /**
125
+ * Standard MCP tool result envelope. Mirrors MCP's `CallToolResult` shape:
126
+ * a `content` array of typed items (text, structured data) plus an optional
127
+ * `isError` flag for soft errors that the LLM should see.
128
+ */
129
+ export interface McpToolResult {
130
+ content: McpToolContent[];
131
+ isError?: boolean;
132
+ /**
133
+ * Optional structured data the LLM can act on programmatically. Mirrors
134
+ * the MCP SDK's `structuredContent` field.
135
+ */
136
+ structuredContent?: Record<string, unknown>;
137
+ }
138
+ export type McpToolContent = {
139
+ type: "text";
140
+ text: string;
141
+ } | {
142
+ type: "resource";
143
+ resource: {
144
+ uri: string;
145
+ mimeType?: string;
146
+ };
147
+ };
148
+ /**
149
+ * Standard error class for tool failures. The dispatcher catches these
150
+ * and translates them into MCP's standard error envelope.
151
+ */
152
+ export declare class McpToolError extends Error {
153
+ readonly code: McpToolErrorCode;
154
+ readonly meta?: Record<string, unknown> | undefined;
155
+ constructor(message: string, code: McpToolErrorCode, meta?: Record<string, unknown> | undefined);
156
+ }
157
+ export type McpToolErrorCode = "MISSING_SERVICE" | "AUTHORIZATION_DENIED" | "NOT_FOUND" | "INVALID_INPUT" | "PROVIDER_ERROR";
158
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MCP tool contract — transport-agnostic shapes for catalog tools.
3
+ *
4
+ * Every catalog tool is defined as a `McpToolDefinition`: a name, a
5
+ * description (shown to the LLM), an input zod schema, and a handler that
6
+ * takes the parsed args + the per-request context and returns a structured
7
+ * result.
8
+ *
9
+ * Templates wire the registry into the actual MCP transport of their
10
+ * choice (the `@modelcontextprotocol/sdk` for stdio/HTTP+SSE, a custom
11
+ * Hono route, etc.). This package's surface ends at the tool definitions.
12
+ *
13
+ * See `docs/architecture/catalog-rag-architecture.md` §3 + §12.
14
+ */
15
+ /**
16
+ * Standard error class for tool failures. The dispatcher catches these
17
+ * and translates them into MCP's standard error envelope.
18
+ */
19
+ export class McpToolError extends Error {
20
+ code;
21
+ meta;
22
+ constructor(message, code, meta) {
23
+ super(message);
24
+ this.code = code;
25
+ this.meta = meta;
26
+ this.name = "McpToolError";
27
+ }
28
+ }
@@ -0,0 +1,8 @@
1
+ export { type McpAvailabilityResult, type McpCatalogServices, type McpQuoteResult, type McpResolvedEntity, type McpToolContent, type McpToolContext, type McpToolDefinition, McpToolError, type McpToolErrorCode, type McpToolHandler, type McpToolResult, } from "./contract.js";
2
+ export { type CreateMcpToolRegistryOptions, createMcpToolRegistry, enforceAudienceAuthorization, type McpToolListEntry, type McpToolRegistry, requireService, } from "./registry.js";
3
+ export { type CheckAvailabilityArgs, checkAvailabilityTool, } from "./tools/check-availability.js";
4
+ export { type GetEntityArgs, getEntityTool } from "./tools/get-entity.js";
5
+ export { type GetQuoteArgs, getQuoteTool } from "./tools/get-quote.js";
6
+ export { type SearchCatalogArgs, searchCatalogTool, } from "./tools/search-catalog.js";
7
+ export { type SuggestAlternativesArgs, suggestAlternativesTool, } from "./tools/suggest-alternatives.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,4BAA4B,EAC5B,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,cAAc,GACf,MAAM,eAAe,CAAA;AAGtB,OAAO,EACL,KAAK,qBAAqB,EAC1B,qBAAqB,GACtB,MAAM,+BAA+B,CAAA;AACtC,OAAO,EAAE,KAAK,aAAa,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACzE,OAAO,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACtE,OAAO,EACL,KAAK,iBAAiB,EACtB,iBAAiB,GAClB,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACL,KAAK,uBAAuB,EAC5B,uBAAuB,GACxB,MAAM,iCAAiC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // Contract types — McpToolDefinition, McpToolHandler, McpToolContext, results.
2
+ export { McpToolError, } from "./contract.js";
3
+ // Registry — register tools, dispatch by name, list, lookup.
4
+ export { createMcpToolRegistry, enforceAudienceAuthorization, requireService, } from "./registry.js";
5
+ // The five canonical catalog tools.
6
+ export { checkAvailabilityTool, } from "./tools/check-availability.js";
7
+ export { getEntityTool } from "./tools/get-entity.js";
8
+ export { getQuoteTool } from "./tools/get-quote.js";
9
+ export { searchCatalogTool, } from "./tools/search-catalog.js";
10
+ export { suggestAlternativesTool, } from "./tools/suggest-alternatives.js";
@@ -0,0 +1,56 @@
1
+ /**
2
+ * MCP tool registry — register tool definitions, dispatch by name with
3
+ * args validation + context injection.
4
+ *
5
+ * The registry is transport-agnostic. Templates wire `dispatchTool` into
6
+ * their MCP transport's `CallTool` handler — typically the
7
+ * `@modelcontextprotocol/sdk` server, but anything that follows the MCP
8
+ * tool-call shape works.
9
+ *
10
+ * See `docs/architecture/catalog-rag-architecture.md` §3.
11
+ */
12
+ import type { z } from "zod";
13
+ import type { McpToolContext, McpToolDefinition, McpToolResult } from "./contract.js";
14
+ export interface McpToolRegistry {
15
+ /** The context used for all tool dispatches in this registry. */
16
+ readonly context: McpToolContext;
17
+ /** Register a tool definition. Throws on duplicate name. */
18
+ register<TArgs>(tool: McpToolDefinition<TArgs, McpToolResult>): void;
19
+ /** List registered tool names — useful for the MCP `ListTools` request. */
20
+ list(): McpToolListEntry[];
21
+ /** Dispatch a tool call by name. Validates args with the tool's zod schema. */
22
+ dispatchTool(name: string, args: unknown): Promise<McpToolResult>;
23
+ /** Look up a registered tool. Returns `undefined` if not registered. */
24
+ get(name: string): McpToolDefinition<any, McpToolResult> | undefined;
25
+ }
26
+ export interface McpToolListEntry {
27
+ name: string;
28
+ description: string;
29
+ /**
30
+ * Zod schema serialized as a placeholder JSON Schema. Templates that
31
+ * need actual JSON Schema for the MCP SDK should call `zod-to-json-schema`
32
+ * (or equivalent) on the tool's `inputSchema` before publishing.
33
+ */
34
+ inputSchemaPreview: {
35
+ type: "object";
36
+ description: string;
37
+ };
38
+ }
39
+ export interface CreateMcpToolRegistryOptions {
40
+ context: McpToolContext;
41
+ }
42
+ export declare function createMcpToolRegistry(options: CreateMcpToolRegistryOptions): McpToolRegistry;
43
+ /**
44
+ * Helper for tools to assert that a required catalog service was injected
45
+ * into the context. Throws `MISSING_SERVICE` if not, which the dispatcher
46
+ * translates into a structured error.
47
+ */
48
+ export declare function requireService<T>(service: T | undefined, name: string): T;
49
+ /**
50
+ * Helper for tools that need to enforce per-actor authorization. Customer/
51
+ * partner/supplier actors are pinned to their own audience pool — staff
52
+ * actors may federate across pools.
53
+ */
54
+ export declare function enforceAudienceAuthorization(actor: McpToolContext["actor"], requestedAudiences?: string[]): void;
55
+ export type { z };
56
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAE5B,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAGrF,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAA;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,GAAG,IAAI,CAAA;IACpE,2EAA2E;IAC3E,IAAI,IAAI,gBAAgB,EAAE,CAAA;IAC1B,+EAA+E;IAC/E,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,aAAa,CAAC,CAAA;IACjE,wEAAwE;IAExE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAAC,GAAG,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;CACrE;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,kBAAkB,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CAC5D;AAED,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,cAAc,CAAA;CACxB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,eAAe,CAuD5F;AAUD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,CASzE;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,EAC9B,kBAAkB,CAAC,EAAE,MAAM,EAAE,GAC5B,IAAI,CASN;AAID,YAAY,EAAE,CAAC,EAAE,CAAA"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * MCP tool registry — register tool definitions, dispatch by name with
3
+ * args validation + context injection.
4
+ *
5
+ * The registry is transport-agnostic. Templates wire `dispatchTool` into
6
+ * their MCP transport's `CallTool` handler — typically the
7
+ * `@modelcontextprotocol/sdk` server, but anything that follows the MCP
8
+ * tool-call shape works.
9
+ *
10
+ * See `docs/architecture/catalog-rag-architecture.md` §3.
11
+ */
12
+ import { McpToolError } from "./contract.js";
13
+ export function createMcpToolRegistry(options) {
14
+ // biome-ignore lint/suspicious/noExplicitAny: heterogeneous tool args
15
+ const tools = new Map();
16
+ return {
17
+ get context() {
18
+ return options.context;
19
+ },
20
+ register(tool) {
21
+ if (tools.has(tool.name)) {
22
+ throw new Error(`MCP tool "${tool.name}" is already registered`);
23
+ }
24
+ tools.set(tool.name, tool);
25
+ },
26
+ list() {
27
+ return Array.from(tools.values()).map((tool) => ({
28
+ name: tool.name,
29
+ description: tool.description,
30
+ inputSchemaPreview: {
31
+ type: "object",
32
+ description: "Use the package's zod-to-json-schema adapter to serialize for the MCP SDK.",
33
+ },
34
+ }));
35
+ },
36
+ get(name) {
37
+ return tools.get(name);
38
+ },
39
+ async dispatchTool(name, args) {
40
+ const tool = tools.get(name);
41
+ if (!tool) {
42
+ return errorResult(`MCP tool "${name}" is not registered. Known tools: ${Array.from(tools.keys()).join(", ") || "(none)"}`, "NOT_FOUND");
43
+ }
44
+ let parsed;
45
+ try {
46
+ parsed = tool.inputSchema.parse(args);
47
+ }
48
+ catch (err) {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ return errorResult(`Invalid input for tool "${name}": ${message}`, "INVALID_INPUT");
51
+ }
52
+ try {
53
+ return await tool.handler(parsed, options.context);
54
+ }
55
+ catch (err) {
56
+ if (err instanceof McpToolError) {
57
+ return errorResult(err.message, err.code, err.meta);
58
+ }
59
+ const message = err instanceof Error ? err.message : String(err);
60
+ return errorResult(`Tool "${name}" failed: ${message}`, "PROVIDER_ERROR");
61
+ }
62
+ },
63
+ };
64
+ }
65
+ function errorResult(message, code, meta) {
66
+ return {
67
+ isError: true,
68
+ content: [{ type: "text", text: `[${code}] ${message}` }],
69
+ structuredContent: { error: { code, message, ...(meta ?? {}) } },
70
+ };
71
+ }
72
+ /**
73
+ * Helper for tools to assert that a required catalog service was injected
74
+ * into the context. Throws `MISSING_SERVICE` if not, which the dispatcher
75
+ * translates into a structured error.
76
+ */
77
+ export function requireService(service, name) {
78
+ if (!service) {
79
+ throw new McpToolError(`MCP tool requires the "${name}" service to be wired into the context, but it was not provided. Configure the registry's catalog services or omit tools that depend on it.`, "MISSING_SERVICE", { service: name });
80
+ }
81
+ return service;
82
+ }
83
+ /**
84
+ * Helper for tools that need to enforce per-actor authorization. Customer/
85
+ * partner/supplier actors are pinned to their own audience pool — staff
86
+ * actors may federate across pools.
87
+ */
88
+ export function enforceAudienceAuthorization(actor, requestedAudiences) {
89
+ if (!requestedAudiences || requestedAudiences.length === 0)
90
+ return;
91
+ if (actor === "staff")
92
+ return;
93
+ if (requestedAudiences.length === 1 && requestedAudiences[0] === actor)
94
+ return;
95
+ throw new McpToolError(`Actor "${actor}" is not authorized to query audiences ${JSON.stringify(requestedAudiences)}. Non-staff actors may only query their own audience pool.`, "AUTHORIZATION_DENIED", { actor, requestedAudiences });
96
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../src/registry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,130 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { z } from "zod";
3
+ import { McpToolError } from "./contract.js";
4
+ import { createMcpToolRegistry, enforceAudienceAuthorization, requireService } from "./registry.js";
5
+ const baseContext = {
6
+ actor: "staff",
7
+ tenantId: "op_test",
8
+ defaultScope: {
9
+ locale: "en-GB",
10
+ audience: "staff",
11
+ market: "default",
12
+ actor: "staff",
13
+ },
14
+ catalog: {},
15
+ };
16
+ const echoTool = {
17
+ name: "echo",
18
+ description: "Echo a message back.",
19
+ inputSchema: z.object({ message: z.string() }),
20
+ async handler(args) {
21
+ return { content: [{ type: "text", text: args.message }] };
22
+ },
23
+ };
24
+ describe("createMcpToolRegistry", () => {
25
+ it("registers and dispatches a tool", async () => {
26
+ const registry = createMcpToolRegistry({ context: baseContext });
27
+ registry.register(echoTool);
28
+ const result = await registry.dispatchTool("echo", { message: "hello" });
29
+ expect(result.content[0]).toEqual({ type: "text", text: "hello" });
30
+ });
31
+ it("throws on duplicate registration", () => {
32
+ const registry = createMcpToolRegistry({ context: baseContext });
33
+ registry.register(echoTool);
34
+ expect(() => registry.register(echoTool)).toThrow(/already registered/);
35
+ });
36
+ it("returns NOT_FOUND for unknown tool names", async () => {
37
+ const registry = createMcpToolRegistry({ context: baseContext });
38
+ const result = await registry.dispatchTool("phantom", {});
39
+ expect(result.isError).toBe(true);
40
+ expect(result.structuredContent?.error).toMatchObject({ code: "NOT_FOUND" });
41
+ });
42
+ it("returns INVALID_INPUT when args fail schema validation", async () => {
43
+ const registry = createMcpToolRegistry({ context: baseContext });
44
+ registry.register(echoTool);
45
+ const result = await registry.dispatchTool("echo", { message: 123 });
46
+ expect(result.isError).toBe(true);
47
+ expect(result.structuredContent?.error).toMatchObject({ code: "INVALID_INPUT" });
48
+ });
49
+ it("translates McpToolError thrown by the handler to its code", async () => {
50
+ const failingTool = {
51
+ name: "fail",
52
+ description: "Always fails.",
53
+ inputSchema: z.object({}),
54
+ async handler() {
55
+ throw new McpToolError("nope", "AUTHORIZATION_DENIED", { reason: "test" });
56
+ },
57
+ };
58
+ const registry = createMcpToolRegistry({ context: baseContext });
59
+ registry.register(failingTool);
60
+ const result = await registry.dispatchTool("fail", {});
61
+ expect(result.isError).toBe(true);
62
+ expect(result.structuredContent?.error).toMatchObject({
63
+ code: "AUTHORIZATION_DENIED",
64
+ message: "nope",
65
+ reason: "test",
66
+ });
67
+ });
68
+ it("translates other thrown errors to PROVIDER_ERROR", async () => {
69
+ const failingTool = {
70
+ name: "boom",
71
+ description: "Throws plain Error.",
72
+ inputSchema: z.object({}),
73
+ async handler() {
74
+ throw new Error("network down");
75
+ },
76
+ };
77
+ const registry = createMcpToolRegistry({ context: baseContext });
78
+ registry.register(failingTool);
79
+ const result = await registry.dispatchTool("boom", {});
80
+ expect(result.isError).toBe(true);
81
+ expect(result.structuredContent?.error).toMatchObject({ code: "PROVIDER_ERROR" });
82
+ });
83
+ it("list() exposes registered tool metadata", () => {
84
+ const registry = createMcpToolRegistry({ context: baseContext });
85
+ registry.register(echoTool);
86
+ const list = registry.list();
87
+ expect(list).toHaveLength(1);
88
+ expect(list[0]?.name).toBe("echo");
89
+ expect(list[0]?.description).toBe("Echo a message back.");
90
+ });
91
+ it("get() returns the registered tool by name", () => {
92
+ const registry = createMcpToolRegistry({ context: baseContext });
93
+ registry.register(echoTool);
94
+ expect(registry.get("echo")?.name).toBe("echo");
95
+ expect(registry.get("phantom")).toBeUndefined();
96
+ });
97
+ });
98
+ describe("requireService", () => {
99
+ it("returns the service when present", () => {
100
+ const indexer = { mock: true };
101
+ expect(requireService(indexer, "indexer")).toBe(indexer);
102
+ });
103
+ it("throws MISSING_SERVICE when undefined", () => {
104
+ expect(() => requireService(undefined, "indexer")).toThrow(McpToolError);
105
+ try {
106
+ requireService(undefined, "indexer");
107
+ }
108
+ catch (err) {
109
+ expect(err.code).toBe("MISSING_SERVICE");
110
+ }
111
+ });
112
+ });
113
+ describe("enforceAudienceAuthorization", () => {
114
+ it("staff actors may query any audience", () => {
115
+ expect(() => enforceAudienceAuthorization("staff", ["customer", "partner"])).not.toThrow();
116
+ });
117
+ it("non-staff actors may query only their own audience", () => {
118
+ expect(() => enforceAudienceAuthorization("customer", ["customer"])).not.toThrow();
119
+ });
120
+ it("non-staff actors trying to query another audience throws", () => {
121
+ expect(() => enforceAudienceAuthorization("customer", ["partner"])).toThrow(/not authorized/);
122
+ });
123
+ it("non-staff actors trying to federate throws", () => {
124
+ expect(() => enforceAudienceAuthorization("customer", ["customer", "partner"])).toThrow(/not authorized/);
125
+ });
126
+ it("empty audience list short-circuits without checks", () => {
127
+ expect(() => enforceAudienceAuthorization("customer", [])).not.toThrow();
128
+ expect(() => enforceAudienceAuthorization("customer", undefined)).not.toThrow();
129
+ });
130
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `check_availability` tool — calls the source adapter's volatile-live
3
+ * availability path. Per architecture, volatile-live values are always
4
+ * fetched live, never indexed, never embedded.
5
+ */
6
+ import { z } from "zod";
7
+ import type { McpToolDefinition, McpToolResult } from "../contract.js";
8
+ declare const checkAvailabilityArgs: z.ZodObject<{
9
+ vertical: z.ZodString;
10
+ entityId: z.ZodString;
11
+ parameters: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
12
+ }, z.core.$strip>;
13
+ export type CheckAvailabilityArgs = z.infer<typeof checkAvailabilityArgs>;
14
+ export declare const checkAvailabilityTool: McpToolDefinition<CheckAvailabilityArgs, McpToolResult>;
15
+ export {};
16
+ //# sourceMappingURL=check-availability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-availability.d.ts","sourceRoot":"","sources":["../../src/tools/check-availability.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAGtE,QAAA,MAAM,qBAAqB;;;;iBASzB,CAAA;AAEF,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAEzE,eAAO,MAAM,qBAAqB,EAAE,iBAAiB,CAAC,qBAAqB,EAAE,aAAa,CAyBzF,CAAA"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `check_availability` tool — calls the source adapter's volatile-live
3
+ * availability path. Per architecture, volatile-live values are always
4
+ * fetched live, never indexed, never embedded.
5
+ */
6
+ import { z } from "zod";
7
+ import { requireService } from "../registry.js";
8
+ const checkAvailabilityArgs = z.object({
9
+ vertical: z.string().describe("The catalog vertical."),
10
+ entityId: z.string().describe("Entity id."),
11
+ parameters: z
12
+ .record(z.string(), z.unknown())
13
+ .default({})
14
+ .describe("Vertical-specific parameters: dates, occupancy, currency, market — whatever the live-availability call needs."),
15
+ });
16
+ export const checkAvailabilityTool = {
17
+ name: "check_availability",
18
+ description: "Check live availability for a catalog entry. Routes through the configured source adapter — " +
19
+ "values are always fresh, never cached at the catalog plane. Use this before suggesting a quote.",
20
+ inputSchema: checkAvailabilityArgs,
21
+ async handler(args, context) {
22
+ const checkAvailability = requireService(context.catalog.checkAvailability, "checkAvailability");
23
+ const result = await checkAvailability(args.vertical, args.entityId, args.parameters);
24
+ const summary = result.available
25
+ ? `${args.vertical}/${args.entityId} is AVAILABLE (checked at ${result.checkedAt}).`
26
+ : `${args.vertical}/${args.entityId} is NOT AVAILABLE (checked at ${result.checkedAt}).`;
27
+ return {
28
+ content: [{ type: "text", text: summary }],
29
+ structuredContent: {
30
+ vertical: args.vertical,
31
+ entityId: args.entityId,
32
+ available: result.available,
33
+ checkedAt: result.checkedAt,
34
+ details: result.details,
35
+ },
36
+ };
37
+ },
38
+ };