@rudderjs/mcp 5.0.0 → 5.1.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 CHANGED
@@ -85,6 +85,38 @@ Don't take a `send` callback parameter — the generator pattern matches
85
85
  `@rudderjs/ai` and lets the runtime choose where progress lands (HTTP SSE,
86
86
  in-process collector, etc.).
87
87
 
88
+ ### Tool annotations (behavior hints)
89
+
90
+ Tools may carry MCP-spec hints that clients (Claude Desktop, Cursor, etc.) use to decide whether to auto-approve a call, batch it, or sandbox it. Apply them as class decorators:
91
+
92
+ ```ts
93
+ import { McpTool, IsReadOnly, IsDestructive, IsIdempotent, IsOpenWorld } from '@rudderjs/mcp'
94
+
95
+ @IsReadOnly()
96
+ @IsIdempotent()
97
+ class GetUserTool extends McpTool { /* ... */ }
98
+
99
+ @IsDestructive()
100
+ @IsOpenWorld()
101
+ class DeleteFileTool extends McpTool { /* ... */ }
102
+ ```
103
+
104
+ These surface as `annotations` on `tools/list` per the MCP spec. The hints are advisory — clients still apply their own policy. Both `true` and `false` are meaningful (vs. omitted), so the decorators accept an explicit value: `@IsReadOnly()` (= true), `@IsReadOnly(false)`, or no decorator (= omitted).
105
+
106
+ ### Conditional registration (`shouldRegister`)
107
+
108
+ Hide a tool, resource, or prompt from listings based on static state — feature flags, env mode, build-time config:
109
+
110
+ ```ts
111
+ class ExperimentalTool extends McpTool {
112
+ schema() { return z.object({}) }
113
+ async handle() { return McpResponse.text('experimental') }
114
+ shouldRegister() { return process.env.FEATURE_EXPERIMENTAL === 'true' }
115
+ }
116
+ ```
117
+
118
+ Returning `false` hides the primitive from `tools/list` AND causes `tools/call` to return "Unknown tool" — preventing bypass via direct call. The same hook is available on `McpResource` and `McpPrompt`. Async hooks are supported.
119
+
88
120
  ### Server-initiated notifications
89
121
 
90
122
  Push events to all connected clients via `McpServer` instance methods. The
@@ -119,6 +151,24 @@ export class WeatherGuidelinesResource extends McpResource {
119
151
  }
120
152
  ```
121
153
 
154
+ ### Resource annotations
155
+
156
+ Resources accept three protocol-level annotations: `@Audience`, `@Priority`, and `@LastModified`. These help clients rank and surface resources in their UI:
157
+
158
+ ```ts
159
+ import { McpResource, Audience, Priority, LastModified } from '@rudderjs/mcp'
160
+
161
+ @Audience('user') // 'user' | 'assistant' | both
162
+ @Priority(0.9) // 0..1 importance score
163
+ @LastModified('2026-05-09T00:00:00Z') // ISO 8601 string or Date
164
+ export class ReleaseNotesResource extends McpResource {
165
+ uri() { return 'file://release-notes' }
166
+ async handle() { return await readReleaseNotes() }
167
+ }
168
+ ```
169
+
170
+ Resources also accept the same `shouldRegister()` hook as tools.
171
+
122
172
  ## Prompts
123
173
 
124
174
  ```ts
@@ -0,0 +1,316 @@
1
+ # @rudderjs/mcp
2
+
3
+ ## Overview
4
+
5
+ MCP (Model Context Protocol) server framework for RudderJS. Lets the app expose **tools**, **resources**, and **prompts** to AI agents (Claude Code, Cursor, etc.) over either HTTP Streamable transport or local stdio. Tools and resources are plain classes with decorator-driven metadata and Zod input schemas. DI resolves constructor dependencies, OAuth 2.1 protects HTTP endpoints via `@rudderjs/passport`, and an observer registry exposes every tool call / resource read / prompt render for Telescope and other collectors.
6
+
7
+ ## Key Patterns
8
+
9
+ ### Server Definition
10
+
11
+ Extend `McpServer` and list the tool/resource/prompt **classes** (not instances). Metadata comes from decorators on the class.
12
+
13
+ ```ts
14
+ import { McpServer, Name, Version, Instructions } from '@rudderjs/mcp'
15
+
16
+ @Name('Weather Server')
17
+ @Version('1.0.0')
18
+ @Instructions('Provide weather information and forecasts.')
19
+ export class WeatherServer extends McpServer {
20
+ protected tools = [CurrentWeatherTool]
21
+ protected resources = [WeatherGuidelinesResource, CityWeatherResource]
22
+ protected prompts = [DescribeWeatherPrompt]
23
+ }
24
+ ```
25
+
26
+ ### Tools
27
+
28
+ Declare input via Zod, return via `McpResponse` helpers. Decorator `@Description` drives what the AI sees.
29
+
30
+ ```ts
31
+ import { McpTool, McpResponse, Description } from '@rudderjs/mcp'
32
+ import { z } from 'zod'
33
+
34
+ @Description('Get current weather for a location.')
35
+ export class CurrentWeatherTool extends McpTool {
36
+ schema() {
37
+ return z.object({
38
+ location: z.string().describe('City name'),
39
+ })
40
+ }
41
+
42
+ async handle(input: Record<string, unknown>) {
43
+ const location = input.location as string
44
+ return McpResponse.text(`Weather in ${location}: sunny, 22°C`)
45
+ }
46
+ }
47
+ ```
48
+
49
+ **Output schemas** — optional, advertises structured response shape to the client:
50
+
51
+ ```ts
52
+ outputSchema() {
53
+ return z.object({ temperature: z.number(), conditions: z.string() })
54
+ }
55
+ // handle() must return JSON matching the schema
56
+ async handle() { return McpResponse.json({ temperature: 22, conditions: 'sunny' }) }
57
+ ```
58
+
59
+ **Behavior annotations (MCP spec hints).** Apply as class decorators — clients use these to decide auto-approval / sandboxing / batching:
60
+
61
+ ```ts
62
+ import { IsReadOnly, IsDestructive, IsIdempotent, IsOpenWorld } from '@rudderjs/mcp'
63
+
64
+ @IsReadOnly() @IsIdempotent() class GetUserTool extends McpTool { /* ... */ }
65
+ @IsDestructive() @IsOpenWorld() class DeleteFileTool extends McpTool { /* ... */ }
66
+ ```
67
+
68
+ Both `true` and `false` are meaningful (vs. omitted). `@IsReadOnly()` = true; `@IsReadOnly(false)` = explicit false.
69
+
70
+ **Conditional registration.** Hide a tool/resource/prompt for static gating (env flags, feature toggles) — `shouldRegister(): boolean | Promise<boolean>`. Returning false hides from `tools/list` AND blocks `tools/call` (no bypass).
71
+
72
+ ### Resources
73
+
74
+ Two shapes — **static URIs** and **URI templates**:
75
+
76
+ ```ts
77
+ // Static URI
78
+ @Description('Weather usage guidelines')
79
+ export class WeatherGuidelinesResource extends McpResource {
80
+ uri() { return 'weather://guidelines' }
81
+ async handle() { return 'Always check conditions before...' }
82
+ }
83
+
84
+ // URI template — {param} placeholders
85
+ @Description('Weather for a specific city')
86
+ export class CityWeatherResource extends McpResource {
87
+ uri() { return 'weather://city/{name}' } // template
88
+ async handle(params?: Record<string, string>) {
89
+ return `Weather in ${params?.name}: sunny, 22°C`
90
+ }
91
+ }
92
+ ```
93
+
94
+ Templates are auto-registered via `ListResourceTemplates`. Params are extracted from the URI and passed to `handle()`.
95
+
96
+ **Resource annotations (MCP spec).** Three class decorators — `@Audience('user' | 'assistant')`, `@Priority(0..1)`, `@LastModified(string | Date)`. They surface in `resources/list`/`resources/templates/list` to help clients rank.
97
+
98
+ ```ts
99
+ @Audience('user') @Priority(0.9) @LastModified(new Date())
100
+ class ReleaseNotesResource extends McpResource { /* ... */ }
101
+ ```
102
+
103
+ Resources also accept the same `shouldRegister()` hook as tools.
104
+
105
+ ### Prompts
106
+
107
+ Like tools, but return message arrays instead of content:
108
+
109
+ ```ts
110
+ import { McpPrompt, Description } from '@rudderjs/mcp'
111
+
112
+ @Description('Describe weather poetically')
113
+ export class DescribeWeatherPrompt extends McpPrompt {
114
+ arguments() { return z.object({ location: z.string() }) }
115
+
116
+ async handle(args: Record<string, unknown>): Promise<McpPromptMessage[]> {
117
+ return [{ role: 'user', content: `Describe weather in ${args.location} poetically.` }]
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### Response Helpers
123
+
124
+ ```ts
125
+ McpResponse.text('Plain text output')
126
+ McpResponse.json({ key: 'value' })
127
+ McpResponse.error('Something went wrong')
128
+ ```
129
+
130
+ ### Dependency Injection
131
+
132
+ Constructor params are auto-resolved from the DI container when the class is instantiated by the runtime. Falls back to `new T()` if the container isn't available.
133
+
134
+ ```ts
135
+ @Injectable()
136
+ @Description('Query the database')
137
+ export class DbQueryTool extends McpTool {
138
+ constructor(private db: DatabaseService) { super() }
139
+
140
+ schema() { return z.object({ query: z.string() }) }
141
+
142
+ async handle(input: Record<string, unknown>) {
143
+ const result = await this.db.query(input.query as string)
144
+ return McpResponse.json(result)
145
+ }
146
+ }
147
+ ```
148
+
149
+ **Method-level DI** — use `@Handle(TokenA, TokenB, ...)` to request DI-resolved extra parameters beyond the first. This is required under Vite/esbuild because those toolchains drop `design:paramtypes` metadata; explicit tokens are the reliable path.
150
+
151
+ ```ts
152
+ @Handle(GreetingService, Logger)
153
+ async handle(input: Record<string, unknown>, greeter: GreetingService, logger: Logger) {
154
+ logger.info(greeter.say(input.name as string))
155
+ return McpResponse.text('ok')
156
+ }
157
+ ```
158
+
159
+ ### Registering Servers
160
+
161
+ ```ts
162
+ import { Mcp } from '@rudderjs/mcp'
163
+ import { WeatherServer } from '../app/Mcp/Servers/WeatherServer.js'
164
+
165
+ // HTTP (Streamable HTTP transport) — served at the given path
166
+ Mcp.web('/mcp/weather', WeatherServer)
167
+
168
+ // With middleware chain
169
+ Mcp.web('/mcp/weather', WeatherServer).middleware([rateLimitMw, loggingMw])
170
+
171
+ // Protected by OAuth 2.1 Bearer tokens (requires @rudderjs/passport)
172
+ Mcp.web('/mcp/weather', WeatherServer).oauth2({ scopes: ['mcp:read'] })
173
+
174
+ // Local stdio (CLI)
175
+ Mcp.local('weather', WeatherServer)
176
+ ```
177
+
178
+ Registration runs once at boot; it's not per-request. Put these calls in a provider's `boot()` method or a dedicated registration module loaded from there.
179
+
180
+ ### OAuth 2.1 Protection
181
+
182
+ `.oauth2()` on the builder chain does three things: validates the Bearer JWT via `@rudderjs/passport`, enforces required scopes, and registers an RFC 9728 **Protected Resource Metadata** endpoint at `/.well-known/oauth-protected-resource<mcp-path>`. On auth failure, the response carries a `WWW-Authenticate` header pointing the client at that metadata doc — standard MCP client discovery flow.
183
+
184
+ ```ts
185
+ Mcp.web('/mcp/admin', AdminServer).oauth2({
186
+ scopes: ['admin'], // required on the token
187
+ authorizationServers: ['https://auth.example.com'], // defaults to app origin
188
+ scopesSupported: ['admin', 'read', 'write'], // advertised in metadata
189
+ })
190
+ ```
191
+
192
+ Passport must be installed and configured — see `@rudderjs/passport` guidelines. Without it, the OAuth middleware returns `invalid_token` with the metadata URL.
193
+
194
+ ### Observer Registry (Telescope Integration)
195
+
196
+ Every tool call, resource read, and prompt render emits a structured event. Packages like Telescope subscribe to collect telemetry; apps can subscribe too for custom logging.
197
+
198
+ ```ts
199
+ import { mcpObservers } from '@rudderjs/mcp/observers'
200
+
201
+ const unsubscribe = mcpObservers.subscribe((event) => {
202
+ // event.kind: 'tool.called' | 'tool.failed' | 'resource.read'
203
+ // | 'resource.failed' | 'prompt.rendered' | 'prompt.failed'
204
+ console.log(`[${event.kind}] ${event.serverName}/${event.name} (${event.duration}ms)`)
205
+ })
206
+ ```
207
+
208
+ The registry is a singleton stored on `globalThis` so state survives Vite SSR module re-evaluation. Observer errors are swallowed inside `emit()` — a broken subscriber cannot break an MCP server.
209
+
210
+ Don't import `@rudderjs/mcp/observers` in normal app code; it's meant for collector packages.
211
+
212
+ ### Config Shape
213
+
214
+ ```ts
215
+ // config/mcp.ts (optional — no required fields today)
216
+ export default {
217
+ // currently no required configuration; provider reads servers from Mcp.web/Mcp.local
218
+ } satisfies Record<string, unknown>
219
+
220
+ // bootstrap/providers.ts — provider is auto-discovered after `rudder providers:discover`
221
+ import { McpProvider } from '@rudderjs/mcp'
222
+ export default [..., McpProvider]
223
+ ```
224
+
225
+ ### CLI Commands
226
+
227
+ ```bash
228
+ pnpm rudder mcp:start <name> # start a local server via stdio
229
+ pnpm rudder mcp:list # list all registered MCP servers (web + local)
230
+ pnpm rudder mcp:inspector <name> # launch the MCP Inspector UI
231
+ ```
232
+
233
+ ### Scaffolders
234
+
235
+ ```bash
236
+ pnpm rudder make:mcp-server Weather
237
+ pnpm rudder make:mcp-tool CurrentWeather
238
+ pnpm rudder make:mcp-resource WeatherGuidelines
239
+ pnpm rudder make:mcp-prompt DescribeWeather
240
+ ```
241
+
242
+ Scaffolders land in `app/Mcp/{Servers,Tools,Resources,Prompts}/`.
243
+
244
+ ### Testing
245
+
246
+ `McpTestClient` boots an in-process server and exposes a minimal protocol client — no network hop, no stdio spawn.
247
+
248
+ ```ts
249
+ import { McpTestClient } from '@rudderjs/mcp'
250
+ import { WeatherServer } from '../app/Mcp/Servers/WeatherServer.js'
251
+
252
+ const client = new McpTestClient(WeatherServer)
253
+
254
+ const result = await client.callTool('current-weather', { location: 'London' })
255
+
256
+ client.assertToolExists('current-weather')
257
+ client.assertToolCount(1)
258
+ client.assertResourceExists('weather://guidelines')
259
+ client.assertPromptExists('describe-weather')
260
+
261
+ const tools = await client.listTools()
262
+ const resources = await client.listResources()
263
+ const prompts = await client.listPrompts()
264
+ ```
265
+
266
+ ## Common Pitfalls
267
+
268
+ - **Register classes, not instances** — `protected tools = [MyTool]` (class), never `[new MyTool()]`. The runtime instantiates each class via DI when the server boots.
269
+ - **`Mcp.web()` / `Mcp.local()` only run once** — at boot. Put them in a provider's `boot()` or a route-loaded module; never in request handlers.
270
+ - **OAuth 2.1 needs Passport** — `.oauth2()` requires `@rudderjs/passport` installed and configured. Without it, every request fails `invalid_token`.
271
+ - **Constructor DI works, but method DI under Vite needs `@Handle(...)`** — esbuild drops `design:paramtypes` metadata, so `@Handle()` without tokens silently falls back to empty. Always pass explicit tokens.
272
+ - **Zod v4 introspection differences** — the JSON-schema converter in `zod-to-json-schema.ts` handles both v3 and v4 shapes (`.describe()` location, `typeName`→`type`, `array.type`→`element`, `enum.values`→`entries`). When extending the converter, support both.
273
+ - **URI templates: only `{param}` placeholders** — no regex, no optional segments. Extracted params are always `string`. Validate/coerce inside `handle()`.
274
+ - **Output schema must match `handle()` return** — declaring `outputSchema()` but returning an unrelated shape surfaces a validation error to the client. Keep them in sync.
275
+ - **`McpResponse.error()` vs throwing** — `McpResponse.error(msg)` returns an MCP-protocol-shaped error response; throwing inside `handle()` emits a `tool.failed` observer event and surfaces a generic error to the client. Prefer `McpResponse.error()` for expected failures, throw for programmer errors.
276
+ - **Don't import `/observers` in app code** — that subpath is for Telescope-style collectors, not for general subscription. If you need tool-call logging in an app, prefer AI middleware or route-level observability instead.
277
+ - **Scaffolder registers via `registerMakeSpecs`** — `make:*` commands skip `bootApp()` for speed. Don't add boot-dependent logic to scaffolder stubs.
278
+ - **Provider boot order** — `McpProvider` registers web routes with the router, so the router provider must boot first. Auto-discovery handles this; don't add `McpProvider` manually before `@rudderjs/router` in a custom provider list.
279
+
280
+ ## Key Imports
281
+
282
+ ```ts
283
+ // Server + building blocks
284
+ import { McpServer, McpTool, McpResource, McpPrompt } from '@rudderjs/mcp'
285
+
286
+ // Response helpers
287
+ import { McpResponse } from '@rudderjs/mcp'
288
+
289
+ // Decorators
290
+ import { Name, Version, Instructions, Description, Handle } from '@rudderjs/mcp'
291
+ // MCP-spec annotations
292
+ import { IsReadOnly, IsDestructive, IsIdempotent, IsOpenWorld } from '@rudderjs/mcp' // tools
293
+ import { Audience, Priority, LastModified } from '@rudderjs/mcp' // resources
294
+
295
+ // Registration facade
296
+ import { Mcp } from '@rudderjs/mcp'
297
+ import type { McpWebEntry, McpWebBuilder } from '@rudderjs/mcp'
298
+
299
+ // OAuth 2.1 protection
300
+ import { oauth2McpMiddleware, registerOAuth2Metadata } from '@rudderjs/mcp'
301
+ import type { OAuth2McpOptions } from '@rudderjs/mcp'
302
+
303
+ // Runtime primitives (rarely needed in app code)
304
+ import { createSdkServer, startStdio, mountHttpTransport } from '@rudderjs/mcp'
305
+ import type { HttpTransportOptions } from '@rudderjs/mcp'
306
+
307
+ // Provider + testing
308
+ import { McpProvider, McpTestClient } from '@rudderjs/mcp'
309
+
310
+ // Observer registry — for collectors only, not app code
311
+ import { mcpObservers } from '@rudderjs/mcp/observers'
312
+ import type { McpObserverEvent, McpObserver, McpObserverRegistry } from '@rudderjs/mcp'
313
+
314
+ // Types
315
+ import type { McpServerMetadata, McpToolResult, McpPromptMessage, InjectToken } from '@rudderjs/mcp'
316
+ ```
@@ -0,0 +1,285 @@
1
+ ---
2
+ name: mcp-servers
3
+ description: Building MCP servers with tools, resources, prompts, decorators, and HTTP/stdio transports in RudderJS
4
+ ---
5
+
6
+ # MCP Servers
7
+
8
+ ## When to use this skill
9
+
10
+ Load this skill when you need to build a Model Context Protocol (MCP) server to expose tools, resources, and prompts to AI coding assistants and other MCP clients.
11
+
12
+ ## Key concepts
13
+
14
+ - **McpServer**: Base class that declares which tools, resources, and prompts to register. Extend it and set `protected tools`, `resources`, and `prompts` arrays.
15
+ - **McpTool**: Base class for tools. Implement `schema()` (Zod) and `handle()`. Name auto-derived from class name (PascalCase -> kebab-case, minus "Tool" suffix).
16
+ - **McpResource**: Base class for resources. Implement `uri()` and `handle()`. Supports URI templates with `{param}` placeholders.
17
+ - **McpPrompt**: Base class for prompts. Implement `handle()` and optionally `arguments()` for a Zod schema.
18
+ - **Decorators**: `@Name`, `@Version`, `@Instructions`, `@Description` set metadata via reflect-metadata.
19
+ - **McpResponse**: Helper for building tool results (`McpResponse.text()`, `.json()`, `.error()`).
20
+ - **Transports**: stdio (local CLI) via `startStdio()`, HTTP/SSE (web) via `mountHttpTransport()`.
21
+ - **DI support**: Tool/resource/prompt classes are resolved via the framework's DI container when available, falling back to plain `new T()`.
22
+ - **McpTestClient**: In-memory test client for unit testing servers without transport overhead.
23
+
24
+ ## Step-by-step
25
+
26
+ ### 1. Create a tool
27
+
28
+ ```ts
29
+ // app/Mcp/Tools/WeatherTool.ts
30
+ import { McpTool, McpResponse, Description } from '@rudderjs/mcp'
31
+ import { z } from 'zod'
32
+
33
+ @Description('Get current weather for a city')
34
+ export class WeatherTool extends McpTool {
35
+ // Name auto-derived: "WeatherTool" -> "weather"
36
+ // Override with: name() { return 'get-weather' }
37
+
38
+ schema() {
39
+ return z.object({
40
+ city: z.string().describe('City name'),
41
+ units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
42
+ })
43
+ }
44
+
45
+ // Optional: advertise the output structure
46
+ outputSchema() {
47
+ return z.object({
48
+ temperature: z.number(),
49
+ conditions: z.string(),
50
+ })
51
+ }
52
+
53
+ async handle(input: Record<string, unknown>) {
54
+ const { city, units } = input as { city: string; units: string }
55
+ const data = await fetchWeather(city, units)
56
+ return McpResponse.json({ temperature: data.temp, conditions: data.conditions })
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### 2. Create a resource
62
+
63
+ ```ts
64
+ // app/Mcp/Resources/SchemaResource.ts
65
+ import { McpResource, Description } from '@rudderjs/mcp'
66
+
67
+ @Description('Returns the database schema')
68
+ export class SchemaResource extends McpResource {
69
+ uri() { return 'db://schema' }
70
+ mimeType() { return 'text/plain' }
71
+
72
+ async handle() {
73
+ const schema = await readFile('prisma/schema.prisma', 'utf-8')
74
+ return schema
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### 3. Create a resource with URI template
80
+
81
+ ```ts
82
+ // app/Mcp/Resources/TableResource.ts
83
+ import { McpResource, Description } from '@rudderjs/mcp'
84
+
85
+ @Description('Read rows from a database table')
86
+ export class TableResource extends McpResource {
87
+ uri() { return 'db://tables/{tableName}' } // {param} makes it a template
88
+ mimeType() { return 'application/json' }
89
+
90
+ async handle(params?: Record<string, string>) {
91
+ const tableName = params?.tableName ?? 'unknown'
92
+ const rows = await db.query(`SELECT * FROM ${tableName} LIMIT 100`)
93
+ return JSON.stringify(rows, null, 2)
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### 4. Create a prompt
99
+
100
+ ```ts
101
+ // app/Mcp/Prompts/ReviewPrompt.ts
102
+ import { McpPrompt, Description } from '@rudderjs/mcp'
103
+ import type { McpPromptMessage } from '@rudderjs/mcp'
104
+ import { z } from 'zod'
105
+
106
+ @Description('Generate a code review prompt for a file')
107
+ export class ReviewPrompt extends McpPrompt {
108
+ // Name auto-derived: "ReviewPrompt" -> "review"
109
+
110
+ arguments() {
111
+ return z.object({
112
+ file: z.string().describe('Path to the file to review'),
113
+ focus: z.string().optional().describe('Area to focus on'),
114
+ })
115
+ }
116
+
117
+ async handle(args: Record<string, unknown>): Promise<McpPromptMessage[]> {
118
+ const { file, focus } = args as { file: string; focus?: string }
119
+ const content = await readFile(file, 'utf-8')
120
+
121
+ return [
122
+ {
123
+ role: 'user',
124
+ content: `Please review this code${focus ? ` with focus on ${focus}` : ''}:\n\n${content}`,
125
+ },
126
+ ]
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### 5. Assemble the server
132
+
133
+ ```ts
134
+ // app/Mcp/AppMcpServer.ts
135
+ import { McpServer, Name, Version, Instructions } from '@rudderjs/mcp'
136
+ import { WeatherTool } from './Tools/WeatherTool.js'
137
+ import { SchemaResource } from './Resources/SchemaResource.js'
138
+ import { TableResource } from './Resources/TableResource.js'
139
+ import { ReviewPrompt } from './Prompts/ReviewPrompt.js'
140
+
141
+ @Name('my-app-mcp')
142
+ @Version('1.0.0')
143
+ @Instructions('An MCP server for my application. Use the weather tool to check conditions.')
144
+ export class AppMcpServer extends McpServer {
145
+ protected tools = [WeatherTool]
146
+ protected resources = [SchemaResource, TableResource]
147
+ protected prompts = [ReviewPrompt]
148
+ }
149
+ ```
150
+
151
+ ### 6. Register for stdio transport (local CLI)
152
+
153
+ ```ts
154
+ // routes/console.ts or bootstrap
155
+ import { Mcp } from '@rudderjs/mcp'
156
+ import { AppMcpServer } from '../app/Mcp/AppMcpServer.js'
157
+
158
+ Mcp.local('app', AppMcpServer)
159
+
160
+ // Run via: pnpm rudder mcp:start app
161
+ ```
162
+
163
+ ### 7. Register for HTTP transport (web endpoint)
164
+
165
+ ```ts
166
+ // routes/console.ts or a provider's boot()
167
+ import { Mcp } from '@rudderjs/mcp'
168
+ import { AppMcpServer } from '../app/Mcp/AppMcpServer.js'
169
+
170
+ Mcp.web('/mcp', AppMcpServer)
171
+
172
+ // Optionally add middleware:
173
+ Mcp.web('/mcp', AppMcpServer).middleware([rateLimitMiddleware])
174
+
175
+ // The endpoint handles:
176
+ // POST /mcp — JSON-RPC messages
177
+ // GET /mcp — SSE stream for server-initiated notifications
178
+ // DELETE /mcp — session termination
179
+ ```
180
+
181
+ ### 8. Register the MCP service provider
182
+
183
+ ```ts
184
+ // bootstrap/providers.ts
185
+ import { mcp } from '@rudderjs/mcp'
186
+
187
+ export default [
188
+ ...(await defaultProviders()),
189
+ mcp(), // registers Mcp facade + boots web/local servers + CLI commands
190
+ ]
191
+ ```
192
+
193
+ ### 9. McpResponse helpers
194
+
195
+ ```ts
196
+ import { McpResponse } from '@rudderjs/mcp'
197
+
198
+ // Text response
199
+ return McpResponse.text('The weather is sunny')
200
+
201
+ // JSON response (pretty-printed)
202
+ return McpResponse.json({ temp: 72, conditions: 'sunny' })
203
+
204
+ // Error response
205
+ return McpResponse.error('City not found')
206
+
207
+ // Raw response (for images etc.)
208
+ return {
209
+ content: [
210
+ { type: 'text', text: 'Here is the chart:' },
211
+ { type: 'image', data: base64Data, mimeType: 'image/png' },
212
+ ],
213
+ }
214
+ ```
215
+
216
+ ### 10. Testing with McpTestClient
217
+
218
+ ```ts
219
+ import { McpTestClient } from '@rudderjs/mcp'
220
+ import { AppMcpServer } from './AppMcpServer.js'
221
+
222
+ const client = new McpTestClient(AppMcpServer)
223
+
224
+ // List tools
225
+ const tools = await client.listTools()
226
+ // [{ name: 'weather', description: 'Get current weather...' }]
227
+
228
+ // Call a tool
229
+ const result = await client.callTool('weather', { city: 'Paris', units: 'celsius' })
230
+
231
+ // Read a resource
232
+ const schema = await client.readResource('db://schema')
233
+
234
+ // Get a prompt
235
+ const messages = await client.getPrompt('review', { file: 'src/index.ts' })
236
+
237
+ // Assertions
238
+ client.assertToolExists('weather')
239
+ client.assertToolCount(1)
240
+ client.assertResourceExists('db://schema')
241
+ client.assertResourceCount(2)
242
+ client.assertPromptExists('review')
243
+ client.assertPromptCount(1)
244
+ ```
245
+
246
+ ### 11. DI-injected tools
247
+
248
+ ```ts
249
+ import { McpTool, McpResponse, Description } from '@rudderjs/mcp'
250
+ import { injectable, inject } from 'tsyringe'
251
+ import { z } from 'zod'
252
+
253
+ @injectable()
254
+ @Description('Search the knowledge base')
255
+ export class SearchTool extends McpTool {
256
+ constructor(@inject('search.service') private search: SearchService) {
257
+ super()
258
+ }
259
+
260
+ schema() {
261
+ return z.object({ query: z.string() })
262
+ }
263
+
264
+ async handle(input: Record<string, unknown>) {
265
+ const results = await this.search.query(input.query as string)
266
+ return McpResponse.json(results)
267
+ }
268
+ }
269
+ // When the RudderJS DI container is available, tools are resolved via
270
+ // container.make(ToolClass), auto-injecting constructor dependencies.
271
+ ```
272
+
273
+ ## Examples
274
+
275
+ See `packages/mcp/src/index.test.ts` for test examples and `packages/mcp/src/runtime.ts` for the full transport implementation.
276
+
277
+ ## Common pitfalls
278
+
279
+ - **Name derivation**: `WeatherTool` -> `weather`, `GetUserInfoTool` -> `get-user-info`. The class name is converted to kebab-case with the `Tool` suffix removed. Override `name()` if the auto-derived name isn't what you want.
280
+ - **Schema must be z.object()**: Both `schema()` and `arguments()` must return `z.object(...)`, not a bare `z.string()` or other type.
281
+ - **@modelcontextprotocol/sdk peer dep**: The MCP SDK (`@modelcontextprotocol/sdk`) is a peer dependency. Install it alongside `@rudderjs/mcp`.
282
+ - **HTTP transport requires @rudderjs/router**: `mountHttpTransport()` dynamically imports `@rudderjs/router` to register the endpoint. Stdio transport has no such requirement.
283
+ - **URI template matching**: Template resources use `{param}` syntax (e.g. `db://tables/{name}`). The extracted params are passed to `handle(params)`.
284
+ - **Error handling**: If `handle()` throws, the runtime catches it and returns `McpResponse.error(err.message)` automatically. You don't need try/catch in every tool.
285
+ - **mcp:list command**: Run `pnpm rudder mcp:list` to see all registered MCP servers (web and local).
@@ -15,5 +15,12 @@ export declare abstract class McpPrompt {
15
15
  * from the DI container when the method is decorated with `@Handle()`.
16
16
  */
17
17
  abstract handle(args: Record<string, unknown>, ...deps: unknown[]): Promise<McpPromptMessage[]>;
18
+ /**
19
+ * Optional hook controlling whether this prompt is exposed to clients.
20
+ *
21
+ * Returning `false` hides the prompt from `prompts/list` AND causes
22
+ * `prompts/get` to throw "Unknown prompt" — preventing bypass.
23
+ */
24
+ shouldRegister?(): boolean | Promise<boolean>;
18
25
  }
19
26
  //# sourceMappingURL=McpPrompt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"McpPrompt.d.ts","sourceRoot":"","sources":["../src/McpPrompt.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,8BAAsB,SAAS;IAC7B,kBAAkB;IAClB,IAAI,IAAI,MAAM;IAId,kBAAkB;IAClB,WAAW,IAAI,MAAM;IAIrB,kDAAkD;IAClD,SAAS,CAAC,IAAI,aAAa;IAE3B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAChG"}
1
+ {"version":3,"file":"McpPrompt.d.ts","sourceRoot":"","sources":["../src/McpPrompt.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAA;IAC1B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,8BAAsB,SAAS;IAC7B,kBAAkB;IAClB,IAAI,IAAI,MAAM;IAId,kBAAkB;IAClB,WAAW,IAAI,MAAM;IAIrB,kDAAkD;IAClD,SAAS,CAAC,IAAI,aAAa;IAE3B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAE/F;;;;;OAKG;IACH,cAAc,CAAC,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;CAC9C"}
@@ -1 +1 @@
1
- {"version":3,"file":"McpPrompt.js","sourceRoot":"","sources":["../src/McpPrompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAQhD,MAAM,OAAgB,SAAS;IAC7B,kBAAkB;IAClB,IAAI;QACF,OAAO,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAA;IAClE,CAAC;IAED,kBAAkB;IAClB,WAAW;QACT,OAAO,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAA;IAC/C,CAAC;CAUF"}
1
+ {"version":3,"file":"McpPrompt.js","sourceRoot":"","sources":["../src/McpPrompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAQhD,MAAM,OAAgB,SAAS;IAC7B,kBAAkB;IAClB,IAAI;QACF,OAAO,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAA;IAClE,CAAC;IAED,kBAAkB;IAClB,WAAW;QACT,OAAO,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAA;IAC/C,CAAC;CAkBF"}