diff --git a/.changeset/mcp-public-introspection-helpers.md b/.changeset/mcp-public-introspection-helpers.md new file mode 100644 index 0000000..1c35de0 --- /dev/null +++ b/.changeset/mcp-public-introspection-helpers.md @@ -0,0 +1,12 @@ +--- +"@gemstack/mcp": minor +--- + +Promote MCP-authoring utilities to the public API so inspectors and tooling no longer need internal access. + +- `McpServer.introspect()`: a public introspection surface returning the registered tool / resource / prompt classes (constructors, not instances) without starting a session. The supported alternative to the internal `_tools()` / `_resources()` / `_prompts()` accessors, which stay `@internal`. +- `zodToJsonSchema(schema)`: convert a Zod schema to the JSON Schema MCP advertises (exported from the package entry). +- `matchUriTemplate(template, uri)`: match a URI against a `resource://{template}` pattern and extract params. +- New `McpServerIntrospection` and `ZodLikeObject` types exported alongside. + +This lets a thin framework binding (e.g. `@rudderjs/mcp`) build a server inspector against the published surface instead of re-declaring internal shapes or carrying local copies of the helpers. diff --git a/packages/mcp/src/McpServer.ts b/packages/mcp/src/McpServer.ts index 3ed687a..c4227a6 100644 --- a/packages/mcp/src/McpServer.ts +++ b/packages/mcp/src/McpServer.ts @@ -10,6 +10,18 @@ export interface McpServerMetadata { instructions?: string } +/** + * The tool / resource / prompt classes a server declares, surfaced for + * inspectors and tooling that enumerate a server without starting a session. + * These are the class constructors (not instances) so a caller can resolve or + * construct them with its own DI. See {@link McpServer.introspect}. + */ +export interface McpServerIntrospection { + tools: (new () => McpTool)[] + resources: (new () => McpResource)[] + prompts: (new () => McpPrompt)[] +} + export interface McpServerOptions { /** * DI resolver used to construct tool / resource / prompt classes and to @@ -89,6 +101,17 @@ export abstract class McpServer { return this.prompts } + /** + * Public introspection surface: the registered tool / resource / prompt + * classes, without starting a session. Returns the class constructors (not + * instances) so inspectors and tooling can resolve or construct them with + * their own DI. This is the supported alternative to the internal + * underscore-prefixed `_tools()` / `_resources()` / `_prompts()` accessors. + */ + introspect(): McpServerIntrospection { + return { tools: this.tools, resources: this.resources, prompts: this.prompts } + } + /** @internal — exposed for tests; counts active notification targets. */ attachedCount(): number { return this._attached?.size ?? 0 diff --git a/packages/mcp/src/index.test.ts b/packages/mcp/src/index.test.ts index c43aeab..fa2cb05 100644 --- a/packages/mcp/src/index.test.ts +++ b/packages/mcp/src/index.test.ts @@ -274,6 +274,68 @@ describe('McpTool', () => { }) }) +// ─── McpServer.introspect ───────────────────────────────── + +describe('McpServer.introspect', () => { + class EchoTool extends McpTool { + schema() { return z.object({ name: z.string() }) } + async handle() { return McpResponse.text('hi') } + } + class DocResource extends McpResource { + uri() { return 'doc://readme' } + async handle() { return '# readme' } + } + class GreetPrompt extends McpPrompt { + async handle() { return [{ role: 'user' as const, content: 'hi' }] } + } + + it('returns the registered tool / resource / prompt classes', () => { + @Name('demo') + class DemoServer extends McpServer { + protected tools = [EchoTool] + protected resources = [DocResource] + protected prompts = [GreetPrompt] + } + + const { tools, resources, prompts } = new DemoServer().introspect() + assert.deepEqual(tools, [EchoTool]) + assert.deepEqual(resources, [DocResource]) + assert.deepEqual(prompts, [GreetPrompt]) + }) + + it('mirrors the @internal _tools()/_resources()/_prompts() accessors', () => { + @Name('demo') + class DemoServer extends McpServer { + protected tools = [EchoTool] + } + + const server = new DemoServer() + const internal = server as unknown as { _tools(): unknown[]; _resources(): unknown[]; _prompts(): unknown[] } + const view = server.introspect() + assert.deepEqual(view.tools, internal._tools()) + assert.deepEqual(view.resources, internal._resources()) + assert.deepEqual(view.prompts, internal._prompts()) + }) + + it('returns empty arrays for a server that declares nothing', () => { + @Name('empty') + class EmptyServer extends McpServer {} + const view = new EmptyServer().introspect() + assert.deepEqual(view, { tools: [], resources: [], prompts: [] }) + }) +}) + +// ─── public MCP-authoring helpers ───────────────────────── + +describe('public helper exports', () => { + it('re-exports zodToJsonSchema and matchUriTemplate from the package entry', async () => { + const entry = await import('./index.js') + assert.equal(typeof entry.zodToJsonSchema, 'function') + assert.equal(typeof entry.matchUriTemplate, 'function') + assert.deepEqual(entry.matchUriTemplate('doc://{id}', 'doc://42'), { id: '42' }) + }) +}) + // ─── McpPrompt ──────────────────────────────────────────── describe('McpPrompt', () => { diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index 7d8a9b6..dcedbf0 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -1,5 +1,5 @@ export { McpServer } from './McpServer.js' -export type { McpServerMetadata, McpServerOptions } from './McpServer.js' +export type { McpServerMetadata, McpServerOptions, McpServerIntrospection } from './McpServer.js' export { McpTool } from './McpTool.js' export type { McpToolResult, McpToolProgress, McpToolReturn } from './McpTool.js' export { McpResource } from './McpResource.js' @@ -31,3 +31,9 @@ export { createMcpHttpHandler } from './runtime/node-handler.js' export { McpTestClient } from './testing.js' export type { McpTestClientOptions } from './testing.js' export type { McpObserverEvent, McpObserver, McpObserverRegistry } from './observers.js' +// MCP-authoring utilities, useful for custom inspectors / tooling built on the +// core: convert a Zod schema to the JSON Schema MCP advertises, and match a URI +// against a `resource://{template}` pattern. Both are pure and dependency-light. +export { zodToJsonSchema } from './zod-to-json-schema.js' +export type { ZodLikeObject } from './types.js' +export { matchUriTemplate } from './uri-template.js'