@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 +50 -0
- package/boost/guidelines.md +316 -0
- package/boost/skills/mcp-servers/SKILL.md +285 -0
- package/dist/McpPrompt.d.ts +7 -0
- package/dist/McpPrompt.d.ts.map +1 -1
- package/dist/McpPrompt.js.map +1 -1
- package/dist/McpResource.d.ts +8 -0
- package/dist/McpResource.d.ts.map +1 -1
- package/dist/McpResource.js.map +1 -1
- package/dist/McpTool.d.ts +11 -0
- package/dist/McpTool.d.ts.map +1 -1
- package/dist/McpTool.js.map +1 -1
- package/dist/decorators.d.ts +28 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +83 -0
- package/dist/decorators.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +10 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +51 -15
- package/dist/runtime.js.map +1 -1
- package/dist/testing.d.ts +3 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +25 -7
- package/dist/testing.js.map +1 -1
- package/package.json +8 -7
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).
|
package/dist/McpPrompt.d.ts
CHANGED
|
@@ -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
|
package/dist/McpPrompt.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/McpPrompt.js.map
CHANGED
|
@@ -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;
|
|
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"}
|