@rudderjs/mcp 5.0.0 → 5.0.1
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/boost/guidelines.md +291 -0
- package/boost/skills/mcp-servers/SKILL.md +285 -0
- package/package.json +8 -7
|
@@ -0,0 +1,291 @@
|
|
|
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
|
+
### Resources
|
|
60
|
+
|
|
61
|
+
Two shapes — **static URIs** and **URI templates**:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// Static URI
|
|
65
|
+
@Description('Weather usage guidelines')
|
|
66
|
+
export class WeatherGuidelinesResource extends McpResource {
|
|
67
|
+
uri() { return 'weather://guidelines' }
|
|
68
|
+
async handle() { return 'Always check conditions before...' }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// URI template — {param} placeholders
|
|
72
|
+
@Description('Weather for a specific city')
|
|
73
|
+
export class CityWeatherResource extends McpResource {
|
|
74
|
+
uri() { return 'weather://city/{name}' } // template
|
|
75
|
+
async handle(params?: Record<string, string>) {
|
|
76
|
+
return `Weather in ${params?.name}: sunny, 22°C`
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Templates are auto-registered via `ListResourceTemplates`. Params are extracted from the URI and passed to `handle()`.
|
|
82
|
+
|
|
83
|
+
### Prompts
|
|
84
|
+
|
|
85
|
+
Like tools, but return message arrays instead of content:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { McpPrompt, Description } from '@rudderjs/mcp'
|
|
89
|
+
|
|
90
|
+
@Description('Describe weather poetically')
|
|
91
|
+
export class DescribeWeatherPrompt extends McpPrompt {
|
|
92
|
+
arguments() { return z.object({ location: z.string() }) }
|
|
93
|
+
|
|
94
|
+
async handle(args: Record<string, unknown>): Promise<McpPromptMessage[]> {
|
|
95
|
+
return [{ role: 'user', content: `Describe weather in ${args.location} poetically.` }]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Response Helpers
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
McpResponse.text('Plain text output')
|
|
104
|
+
McpResponse.json({ key: 'value' })
|
|
105
|
+
McpResponse.error('Something went wrong')
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Dependency Injection
|
|
109
|
+
|
|
110
|
+
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.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
@Injectable()
|
|
114
|
+
@Description('Query the database')
|
|
115
|
+
export class DbQueryTool extends McpTool {
|
|
116
|
+
constructor(private db: DatabaseService) { super() }
|
|
117
|
+
|
|
118
|
+
schema() { return z.object({ query: z.string() }) }
|
|
119
|
+
|
|
120
|
+
async handle(input: Record<string, unknown>) {
|
|
121
|
+
const result = await this.db.query(input.query as string)
|
|
122
|
+
return McpResponse.json(result)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**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.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
@Handle(GreetingService, Logger)
|
|
131
|
+
async handle(input: Record<string, unknown>, greeter: GreetingService, logger: Logger) {
|
|
132
|
+
logger.info(greeter.say(input.name as string))
|
|
133
|
+
return McpResponse.text('ok')
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Registering Servers
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
import { Mcp } from '@rudderjs/mcp'
|
|
141
|
+
import { WeatherServer } from '../app/Mcp/Servers/WeatherServer.js'
|
|
142
|
+
|
|
143
|
+
// HTTP (Streamable HTTP transport) — served at the given path
|
|
144
|
+
Mcp.web('/mcp/weather', WeatherServer)
|
|
145
|
+
|
|
146
|
+
// With middleware chain
|
|
147
|
+
Mcp.web('/mcp/weather', WeatherServer).middleware([rateLimitMw, loggingMw])
|
|
148
|
+
|
|
149
|
+
// Protected by OAuth 2.1 Bearer tokens (requires @rudderjs/passport)
|
|
150
|
+
Mcp.web('/mcp/weather', WeatherServer).oauth2({ scopes: ['mcp:read'] })
|
|
151
|
+
|
|
152
|
+
// Local stdio (CLI)
|
|
153
|
+
Mcp.local('weather', WeatherServer)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
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.
|
|
157
|
+
|
|
158
|
+
### OAuth 2.1 Protection
|
|
159
|
+
|
|
160
|
+
`.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.
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
Mcp.web('/mcp/admin', AdminServer).oauth2({
|
|
164
|
+
scopes: ['admin'], // required on the token
|
|
165
|
+
authorizationServers: ['https://auth.example.com'], // defaults to app origin
|
|
166
|
+
scopesSupported: ['admin', 'read', 'write'], // advertised in metadata
|
|
167
|
+
})
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Passport must be installed and configured — see `@rudderjs/passport` guidelines. Without it, the OAuth middleware returns `invalid_token` with the metadata URL.
|
|
171
|
+
|
|
172
|
+
### Observer Registry (Telescope Integration)
|
|
173
|
+
|
|
174
|
+
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.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { mcpObservers } from '@rudderjs/mcp/observers'
|
|
178
|
+
|
|
179
|
+
const unsubscribe = mcpObservers.subscribe((event) => {
|
|
180
|
+
// event.kind: 'tool.called' | 'tool.failed' | 'resource.read'
|
|
181
|
+
// | 'resource.failed' | 'prompt.rendered' | 'prompt.failed'
|
|
182
|
+
console.log(`[${event.kind}] ${event.serverName}/${event.name} (${event.duration}ms)`)
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
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.
|
|
187
|
+
|
|
188
|
+
Don't import `@rudderjs/mcp/observers` in normal app code; it's meant for collector packages.
|
|
189
|
+
|
|
190
|
+
### Config Shape
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// config/mcp.ts (optional — no required fields today)
|
|
194
|
+
export default {
|
|
195
|
+
// currently no required configuration; provider reads servers from Mcp.web/Mcp.local
|
|
196
|
+
} satisfies Record<string, unknown>
|
|
197
|
+
|
|
198
|
+
// bootstrap/providers.ts — provider is auto-discovered after `rudder providers:discover`
|
|
199
|
+
import { McpProvider } from '@rudderjs/mcp'
|
|
200
|
+
export default [..., McpProvider]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### CLI Commands
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pnpm rudder mcp:start <name> # start a local server via stdio
|
|
207
|
+
pnpm rudder mcp:list # list all registered MCP servers (web + local)
|
|
208
|
+
pnpm rudder mcp:inspector <name> # launch the MCP Inspector UI
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Scaffolders
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pnpm rudder make:mcp-server Weather
|
|
215
|
+
pnpm rudder make:mcp-tool CurrentWeather
|
|
216
|
+
pnpm rudder make:mcp-resource WeatherGuidelines
|
|
217
|
+
pnpm rudder make:mcp-prompt DescribeWeather
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Scaffolders land in `app/Mcp/{Servers,Tools,Resources,Prompts}/`.
|
|
221
|
+
|
|
222
|
+
### Testing
|
|
223
|
+
|
|
224
|
+
`McpTestClient` boots an in-process server and exposes a minimal protocol client — no network hop, no stdio spawn.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import { McpTestClient } from '@rudderjs/mcp'
|
|
228
|
+
import { WeatherServer } from '../app/Mcp/Servers/WeatherServer.js'
|
|
229
|
+
|
|
230
|
+
const client = new McpTestClient(WeatherServer)
|
|
231
|
+
|
|
232
|
+
const result = await client.callTool('current-weather', { location: 'London' })
|
|
233
|
+
|
|
234
|
+
client.assertToolExists('current-weather')
|
|
235
|
+
client.assertToolCount(1)
|
|
236
|
+
client.assertResourceExists('weather://guidelines')
|
|
237
|
+
client.assertPromptExists('describe-weather')
|
|
238
|
+
|
|
239
|
+
const tools = await client.listTools()
|
|
240
|
+
const resources = await client.listResources()
|
|
241
|
+
const prompts = await client.listPrompts()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Common Pitfalls
|
|
245
|
+
|
|
246
|
+
- **Register classes, not instances** — `protected tools = [MyTool]` (class), never `[new MyTool()]`. The runtime instantiates each class via DI when the server boots.
|
|
247
|
+
- **`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.
|
|
248
|
+
- **OAuth 2.1 needs Passport** — `.oauth2()` requires `@rudderjs/passport` installed and configured. Without it, every request fails `invalid_token`.
|
|
249
|
+
- **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.
|
|
250
|
+
- **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.
|
|
251
|
+
- **URI templates: only `{param}` placeholders** — no regex, no optional segments. Extracted params are always `string`. Validate/coerce inside `handle()`.
|
|
252
|
+
- **Output schema must match `handle()` return** — declaring `outputSchema()` but returning an unrelated shape surfaces a validation error to the client. Keep them in sync.
|
|
253
|
+
- **`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.
|
|
254
|
+
- **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.
|
|
255
|
+
- **Scaffolder registers via `registerMakeSpecs`** — `make:*` commands skip `bootApp()` for speed. Don't add boot-dependent logic to scaffolder stubs.
|
|
256
|
+
- **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.
|
|
257
|
+
|
|
258
|
+
## Key Imports
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// Server + building blocks
|
|
262
|
+
import { McpServer, McpTool, McpResource, McpPrompt } from '@rudderjs/mcp'
|
|
263
|
+
|
|
264
|
+
// Response helpers
|
|
265
|
+
import { McpResponse } from '@rudderjs/mcp'
|
|
266
|
+
|
|
267
|
+
// Decorators
|
|
268
|
+
import { Name, Version, Instructions, Description, Handle } from '@rudderjs/mcp'
|
|
269
|
+
|
|
270
|
+
// Registration facade
|
|
271
|
+
import { Mcp } from '@rudderjs/mcp'
|
|
272
|
+
import type { McpWebEntry, McpWebBuilder } from '@rudderjs/mcp'
|
|
273
|
+
|
|
274
|
+
// OAuth 2.1 protection
|
|
275
|
+
import { oauth2McpMiddleware, registerOAuth2Metadata } from '@rudderjs/mcp'
|
|
276
|
+
import type { OAuth2McpOptions } from '@rudderjs/mcp'
|
|
277
|
+
|
|
278
|
+
// Runtime primitives (rarely needed in app code)
|
|
279
|
+
import { createSdkServer, startStdio, mountHttpTransport } from '@rudderjs/mcp'
|
|
280
|
+
import type { HttpTransportOptions } from '@rudderjs/mcp'
|
|
281
|
+
|
|
282
|
+
// Provider + testing
|
|
283
|
+
import { McpProvider, McpTestClient } from '@rudderjs/mcp'
|
|
284
|
+
|
|
285
|
+
// Observer registry — for collectors only, not app code
|
|
286
|
+
import { mcpObservers } from '@rudderjs/mcp/observers'
|
|
287
|
+
import type { McpObserverEvent, McpObserver, McpObserverRegistry } from '@rudderjs/mcp'
|
|
288
|
+
|
|
289
|
+
// Types
|
|
290
|
+
import type { McpServerMetadata, McpToolResult, McpPromptMessage, InjectToken } from '@rudderjs/mcp'
|
|
291
|
+
```
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rudderjs/mcp",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server framework for RudderJS",
|
|
5
5
|
"rudderjs": {
|
|
6
6
|
"provider": "McpProvider",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
},
|
|
15
15
|
"type": "module",
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"boost"
|
|
18
19
|
],
|
|
19
20
|
"main": "./dist/index.js",
|
|
20
21
|
"types": "./dist/index.d.ts",
|
|
@@ -48,11 +49,11 @@
|
|
|
48
49
|
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
49
50
|
"reflect-metadata": "^0.2.0",
|
|
50
51
|
"zod": "^3.23.0",
|
|
51
|
-
"@rudderjs/core": "^1.
|
|
52
|
+
"@rudderjs/core": "^1.1.3"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"@rudderjs/router": "^1.
|
|
55
|
-
"@rudderjs/console": "^1.0.
|
|
55
|
+
"@rudderjs/router": "^1.2.0",
|
|
56
|
+
"@rudderjs/console": "^1.0.1"
|
|
56
57
|
},
|
|
57
58
|
"peerDependenciesMeta": {
|
|
58
59
|
"@rudderjs/router": {
|
|
@@ -65,8 +66,8 @@
|
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@types/node": "^20.0.0",
|
|
67
68
|
"typescript": "^5.4.0",
|
|
68
|
-
"@rudderjs/router": "^1.
|
|
69
|
-
"@rudderjs/console": "^1.0.
|
|
69
|
+
"@rudderjs/router": "^1.2.0",
|
|
70
|
+
"@rudderjs/console": "^1.0.1"
|
|
70
71
|
},
|
|
71
72
|
"author": "Suleiman Shahbari",
|
|
72
73
|
"scripts": {
|