@jaypie/mcp 0.2.12 → 0.3.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.
@@ -0,0 +1,249 @@
1
+ ---
2
+ description: MCP server package with documentation, AWS, Datadog, and LLM tools
3
+ include: "packages/mcp/**"
4
+ ---
5
+
6
+ # @jaypie/mcp Package
7
+
8
+ The `@jaypie/mcp` package provides a Model Context Protocol (MCP) server for AI agents working with Jaypie projects. It includes tools for accessing documentation, AWS CLI operations, Datadog observability, and LLM debugging.
9
+
10
+ ## Package Structure
11
+
12
+ ```
13
+ packages/mcp/
14
+ ├── src/
15
+ │ ├── index.ts # CLI entrypoint, exports createMcpServer and mcpExpressHandler
16
+ │ ├── createMcpServer.ts # Core MCP server factory with tool definitions
17
+ │ ├── mcpExpressHandler.ts # Express middleware for HTTP transport
18
+ │ ├── aws.ts # AWS CLI integration (spawn-based)
19
+ │ ├── datadog.ts # Datadog API integration (https-based)
20
+ │ └── llm.ts # LLM debug utilities
21
+ ├── prompts/ # Markdown guides served via list_prompts/read_prompt tools
22
+ └── dist/ # Built output
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### CLI (stdio transport)
28
+
29
+ ```bash
30
+ npx jaypie-mcp # Run MCP server via stdio
31
+ npx jaypie-mcp --verbose # Run with debug logging
32
+ ```
33
+
34
+ ### Express Integration (HTTP transport)
35
+
36
+ ```typescript
37
+ import express from "express";
38
+ import { mcpExpressHandler } from "@jaypie/mcp";
39
+
40
+ const app = express();
41
+ app.use(express.json());
42
+ app.use("/mcp", await mcpExpressHandler({ version: "1.0.0" }));
43
+ ```
44
+
45
+ ### Direct Server Creation
46
+
47
+ ```typescript
48
+ import { createMcpServer } from "@jaypie/mcp";
49
+
50
+ const server = createMcpServer({ version: "1.0.0", verbose: true });
51
+ ```
52
+
53
+ ## MCP Tools (27 total)
54
+
55
+ ### Documentation Tools (3)
56
+
57
+ | Tool | Description |
58
+ |------|-------------|
59
+ | `list_prompts` | Lists all `.md` files in `prompts/` with descriptions and required file patterns |
60
+ | `read_prompt` | Returns content of a specific prompt file |
61
+ | `version` | Returns package version string |
62
+
63
+ ### AWS CLI Tools (16)
64
+
65
+ Requires AWS CLI installed and configured. All tools accept optional `profile` and `region` parameters.
66
+
67
+ | Tool | Description |
68
+ |------|-------------|
69
+ | `aws_list_profiles` | List available AWS profiles from ~/.aws/config and credentials |
70
+ | `aws_stepfunctions_list_executions` | List Step Function executions for a state machine |
71
+ | `aws_stepfunctions_stop_execution` | Stop a running Step Function execution |
72
+ | `aws_lambda_list_functions` | List Lambda functions with optional prefix filtering |
73
+ | `aws_lambda_get_function` | Get configuration and details for a specific Lambda function |
74
+ | `aws_logs_filter_log_events` | Search CloudWatch Logs with pattern and time range filtering |
75
+ | `aws_s3_list_objects` | List objects in an S3 bucket with optional prefix filtering |
76
+ | `aws_cloudformation_describe_stack` | Get details and status of a CloudFormation stack |
77
+ | `aws_dynamodb_describe_table` | Get metadata about a DynamoDB table |
78
+ | `aws_dynamodb_scan` | Scan a DynamoDB table (use sparingly on large tables) |
79
+ | `aws_dynamodb_query` | Query a DynamoDB table by partition key |
80
+ | `aws_dynamodb_get_item` | Get a single item from a DynamoDB table by primary key |
81
+ | `aws_sqs_list_queues` | List SQS queues with optional prefix filtering |
82
+ | `aws_sqs_get_queue_attributes` | Get queue attributes including message counts |
83
+ | `aws_sqs_receive_message` | Peek at messages in an SQS queue (does not delete) |
84
+ | `aws_sqs_purge_queue` | Delete all messages from an SQS queue (irreversible) |
85
+
86
+ ### Datadog Tools (6)
87
+
88
+ Requires `DATADOG_API_KEY` and `DATADOG_APP_KEY` environment variables.
89
+
90
+ | Tool | Description |
91
+ |------|-------------|
92
+ | `datadog_logs` | Search individual log entries |
93
+ | `datadog_log_analytics` | Aggregate logs with groupBy operations |
94
+ | `datadog_monitors` | List and filter monitors by status/tags |
95
+ | `datadog_synthetics` | List synthetic tests or get results for a specific test |
96
+ | `datadog_metrics` | Query timeseries metrics |
97
+ | `datadog_rum` | Search Real User Monitoring events |
98
+
99
+ ### LLM Tools (2)
100
+
101
+ | Tool | Description |
102
+ |------|-------------|
103
+ | `llm_debug_call` | Debug LLM API calls and inspect raw responses |
104
+ | `llm_list_providers` | List available LLM providers with their models |
105
+
106
+ ## Environment Variables
107
+
108
+ ### AWS CLI Integration
109
+
110
+ | Variable | Description |
111
+ |----------|-------------|
112
+ | `AWS_PROFILE` | Default profile if not specified per-call |
113
+ | `AWS_REGION` or `AWS_DEFAULT_REGION` | Default region if not specified |
114
+
115
+ AWS tools use the host's existing credential chain:
116
+ - `~/.aws/credentials` and `~/.aws/config` files
117
+ - Environment variables (`AWS_ACCESS_KEY_ID`, etc.)
118
+ - SSO sessions established via `aws sso login`
119
+
120
+ ### Datadog Integration
121
+
122
+ | Variable | Description |
123
+ |----------|-------------|
124
+ | `DATADOG_API_KEY` or `DD_API_KEY` | Datadog API key |
125
+ | `DATADOG_APP_KEY` or `DD_APP_KEY` | Datadog Application key |
126
+ | `DD_ENV` | Default environment filter |
127
+ | `DD_SERVICE` | Default service filter |
128
+ | `DD_SOURCE` | Default log source (defaults to "lambda") |
129
+ | `DD_QUERY` | Default query terms appended to searches |
130
+
131
+ ## Adding New Tools
132
+
133
+ Tools are registered in `createMcpServer.ts` using the MCP SDK pattern:
134
+
135
+ ```typescript
136
+ server.tool(
137
+ "tool_name",
138
+ "Description shown to AI agents",
139
+ {
140
+ // Zod schema for parameters
141
+ param1: z.string().describe("Parameter description"),
142
+ param2: z.number().optional().describe("Optional parameter"),
143
+ },
144
+ async ({ param1, param2 }) => {
145
+ // Implementation
146
+ return {
147
+ content: [{ type: "text" as const, text: "Result text" }],
148
+ };
149
+ },
150
+ );
151
+ ```
152
+
153
+ ### AWS Tool Pattern
154
+
155
+ AWS tools use `child_process.spawn` to call the AWS CLI:
156
+
157
+ ```typescript
158
+ // In aws.ts
159
+ export async function myAwsOperation(
160
+ options: MyOperationOptions,
161
+ logger: Logger = nullLogger,
162
+ ): Promise<AwsCommandResult<MyResultType>> {
163
+ const args = ["--required-arg", options.requiredArg];
164
+ if (options.optionalArg) {
165
+ args.push("--optional-arg", options.optionalArg);
166
+ }
167
+
168
+ return executeAwsCommand(
169
+ "service-name", // e.g., "stepfunctions", "lambda", "s3api"
170
+ "command-name", // e.g., "list-executions", "get-function"
171
+ args,
172
+ { profile: options.profile, region: options.region },
173
+ logger,
174
+ );
175
+ }
176
+ ```
177
+
178
+ ### Datadog Tool Pattern
179
+
180
+ Datadog tools use Node.js `https` module directly:
181
+
182
+ ```typescript
183
+ // In datadog.ts
184
+ export async function myDatadogOperation(
185
+ credentials: DatadogCredentials,
186
+ options: MyOptions,
187
+ logger: Logger = nullLogger,
188
+ ): Promise<MyResult> {
189
+ const requestOptions = {
190
+ hostname: "api.datadoghq.com",
191
+ port: 443,
192
+ path: "/api/v2/endpoint",
193
+ method: "POST",
194
+ headers: {
195
+ "DD-API-KEY": credentials.apiKey,
196
+ "DD-APPLICATION-KEY": credentials.appKey,
197
+ "Content-Type": "application/json",
198
+ },
199
+ };
200
+
201
+ return new Promise((resolve) => {
202
+ const req = https.request(requestOptions, (res) => {
203
+ // Handle response
204
+ });
205
+ req.write(JSON.stringify(body));
206
+ req.end();
207
+ });
208
+ }
209
+ ```
210
+
211
+ ## Adding New Prompts
212
+
213
+ Prompts are markdown files in `prompts/` with optional YAML frontmatter:
214
+
215
+ ```yaml
216
+ ---
217
+ description: Brief description shown in list_prompts
218
+ include: "packages/express/**" # File patterns this guide applies to
219
+ ---
220
+
221
+ # Prompt Title
222
+
223
+ Markdown content here...
224
+ ```
225
+
226
+ Prompts are automatically available via `list_prompts` and `read_prompt` tools.
227
+
228
+ ## Exports
229
+
230
+ ```typescript
231
+ import { createMcpServer, mcpExpressHandler } from "@jaypie/mcp";
232
+ import type { CreateMcpServerOptions, McpExpressHandlerOptions } from "@jaypie/mcp";
233
+ ```
234
+
235
+ ## Commands
236
+
237
+ ```bash
238
+ npm run build -w packages/mcp # Build with rollup
239
+ npm run test -w packages/mcp # Run tests (vitest run)
240
+ npm run typecheck -w packages/mcp # Type check (tsc --noEmit)
241
+ npm run format packages/mcp # Format with eslint --fix
242
+ ```
243
+
244
+ ## Security Considerations
245
+
246
+ 1. **Allowlisted operations only** - No arbitrary command execution
247
+ 2. **Read-heavy design** - Most tools are read-only; mutating operations have explicit warnings
248
+ 3. **No credential exposure** - Credentials never passed through MCP; uses host's credential chain
249
+ 4. **Profile isolation** - Each call can specify a different profile for multi-account work
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Complete guide to streaming in Jaypie - Lambda, Express, and SSE patterns
2
+ description: Complete guide to streaming in Jaypie - Lambda, Express, SSE, and NLJSON patterns
3
3
  globs: packages/express/**, packages/lambda/**, packages/aws/**
4
4
  ---
5
5
 
@@ -7,6 +7,23 @@ globs: packages/express/**, packages/lambda/**, packages/aws/**
7
7
 
8
8
  Jaypie provides three distinct streaming patterns for different deployment scenarios. This guide explains when to use each approach and how to implement streaming correctly.
9
9
 
10
+ ## Stream Formats
11
+
12
+ Jaypie supports two stream output formats:
13
+
14
+ | Format | Content-Type | Use Case |
15
+ |--------|--------------|----------|
16
+ | SSE (default) | `text/event-stream` | Browser EventSource, real-time UI updates |
17
+ | NLJSON | `application/x-ndjson` | Machine-to-machine, log processing |
18
+
19
+ ### Format Comparison
20
+
21
+ | Aspect | SSE | NLJSON |
22
+ |--------|-----|--------|
23
+ | Data format | `event: <type>\ndata: {...}\n\n` | `{...}\n` |
24
+ | Error format | `event: error\ndata: {...}\n\n` | `{"error":{...}}\n` |
25
+ | Browser support | Native EventSource API | Requires manual parsing |
26
+
10
27
  ## Quick Reference
11
28
 
12
29
  | Handler | Package | Express Required | Use Case |
@@ -31,8 +48,11 @@ All streaming utilities are exported from `@jaypie/aws` and re-exported through
31
48
  | `createExpressStream(stream, res)` | Pipe async iterable to Express response with SSE headers |
32
49
  | `JaypieStream` | Wrapper class with `.toLambda()` and `.toExpress()` methods |
33
50
  | `createJaypieStream(source)` | Factory function for JaypieStream |
34
- | `formatSSE(chunk)` | Format a chunk as SSE event string |
35
- | `streamToSSE(stream)` | Convert async iterable to SSE-formatted strings |
51
+ | `formatSse(chunk)` | Format a chunk as SSE event string |
52
+ | `formatNljson(chunk)` | Format a chunk as NLJSON string |
53
+ | `formatStreamError(errorBody, format)` | Format error based on stream format |
54
+ | `getContentTypeForFormat(format)` | Get content type for stream format |
55
+ | `streamToSse(stream)` | Convert async iterable to SSE-formatted strings |
36
56
 
37
57
  ### Types
38
58
 
@@ -40,8 +60,9 @@ All streaming utilities are exported from `@jaypie/aws` and re-exported through
40
60
  import type {
41
61
  ExpressStreamResponse,
42
62
  LambdaStreamWriter,
43
- SSEEvent,
63
+ SseEvent,
44
64
  StreamChunk,
65
+ StreamFormat, // "sse" | "nljson"
45
66
  } from "jaypie";
46
67
  ```
47
68
 
@@ -49,13 +70,16 @@ import type {
49
70
 
50
71
  Use for pure Lambda functions without Express. Requires AWS Lambda Response Streaming via Function URL.
51
72
 
73
+ **Note:** `lambdaStreamHandler` automatically wraps with `awslambda.streamifyResponse()` in the Lambda runtime. You no longer need to wrap manually.
74
+
52
75
  ### Basic Usage
53
76
 
54
77
  ```typescript
55
78
  import { lambdaStreamHandler } from "jaypie";
56
79
  import type { StreamHandlerContext } from "@jaypie/lambda";
57
80
 
58
- const streamWorker = lambdaStreamHandler(
81
+ // Auto-wrapped with awslambda.streamifyResponse() in Lambda runtime
82
+ export const handler = lambdaStreamHandler(
59
83
  async (event: unknown, context: StreamHandlerContext) => {
60
84
  const { responseStream } = context;
61
85
 
@@ -70,13 +94,18 @@ const streamWorker = lambdaStreamHandler(
70
94
  },
71
95
  {
72
96
  name: "streamWorker",
73
- contentType: "text/event-stream",
74
97
  }
75
98
  );
99
+ ```
76
100
 
77
- // Wrap with AWS streamifyResponse for Lambda
78
- declare const awslambda: { streamifyResponse: <T>(handler: T) => T };
79
- export const handler = awslambda.streamifyResponse(streamWorker);
101
+ ### With Format Option
102
+
103
+ ```typescript
104
+ // SSE format (default)
105
+ export const sseHandler = lambdaStreamHandler(myHandler, { format: "sse" });
106
+
107
+ // NLJSON format
108
+ export const nljsonHandler = lambdaStreamHandler(myHandler, { format: "nljson" });
80
109
  ```
81
110
 
82
111
  ### With LLM Streaming
@@ -89,7 +118,8 @@ interface PromptEvent {
89
118
  prompt?: string;
90
119
  }
91
120
 
92
- const llmStreamHandler = lambdaStreamHandler(
121
+ // Auto-wrapped with awslambda.streamifyResponse() in Lambda runtime
122
+ export const handler = lambdaStreamHandler(
93
123
  async (event: PromptEvent, context: StreamHandlerContext) => {
94
124
  const llm = new Llm("anthropic");
95
125
  const stream = llm.stream(event.prompt || "Hello");
@@ -102,25 +132,23 @@ const llmStreamHandler = lambdaStreamHandler(
102
132
  secrets: ["ANTHROPIC_API_KEY"],
103
133
  }
104
134
  );
105
-
106
- declare const awslambda: { streamifyResponse: <T>(handler: T) => T };
107
- export const handler = awslambda.streamifyResponse(llmStreamHandler);
108
135
  ```
109
136
 
110
137
  ### Handler Options
111
138
 
112
139
  ```typescript
113
- import type { LambdaStreamHandlerOptions } from "@jaypie/lambda";
140
+ import type { LambdaStreamHandlerOptions, StreamFormat } from "@jaypie/lambda";
114
141
 
115
142
  const options: LambdaStreamHandlerOptions = {
116
143
  name: "myStreamHandler", // Handler name for logging
117
- contentType: "text/event-stream", // Response content type (default)
144
+ format: "sse", // Stream format: "sse" (default) or "nljson"
145
+ contentType: "text/event-stream", // Response content type (auto-set from format)
118
146
  chaos: "low", // Chaos testing level
119
147
  secrets: ["API_KEY"], // AWS secrets to load into process.env
120
148
  setup: [], // Setup function(s)
121
149
  teardown: [], // Teardown function(s)
122
150
  validate: [], // Validation function(s)
123
- throw: false, // Re-throw errors instead of SSE error
151
+ throw: false, // Re-throw errors instead of streaming error
124
152
  unavailable: false, // Return 503 if true
125
153
  };
126
154
  ```
@@ -137,11 +165,19 @@ const streamingLambda = new JaypieLambda(this, "StreamingFunction", {
137
165
  timeout: Duration.minutes(5),
138
166
  });
139
167
 
140
- // Enable Lambda Response Streaming
168
+ // For direct Function URL access:
141
169
  streamingLambda.addFunctionUrl({
142
170
  authType: FunctionUrlAuthType.NONE,
143
171
  invokeMode: InvokeMode.RESPONSE_STREAM,
144
172
  });
173
+
174
+ // Or use JaypieDistribution with streaming: true
175
+ new JaypieDistribution(this, "Distribution", {
176
+ handler: streamingLambda,
177
+ streaming: true,
178
+ host: "api.example.com",
179
+ zone: "example.com",
180
+ });
145
181
  ```
146
182
 
147
183
  ## Pattern 2: expressStreamHandler
@@ -180,14 +216,25 @@ const llmStreamRoute = expressStreamHandler(async (req: Request, res: Response)
180
216
  app.post("/chat", llmStreamRoute);
181
217
  ```
182
218
 
219
+ ### With Format Option
220
+
221
+ ```typescript
222
+ // SSE format (default)
223
+ app.get("/stream-sse", expressStreamHandler(myHandler, { format: "sse" }));
224
+
225
+ // NLJSON format
226
+ app.get("/stream-nljson", expressStreamHandler(myHandler, { format: "nljson" }));
227
+ ```
228
+
183
229
  ### Handler Options
184
230
 
185
231
  ```typescript
186
- import type { ExpressStreamHandlerOptions } from "jaypie";
232
+ import type { ExpressStreamHandlerOptions, StreamFormat } from "jaypie";
187
233
 
188
234
  const options: ExpressStreamHandlerOptions = {
189
235
  name: "myStreamHandler", // Handler name for logging
190
- contentType: "text/event-stream", // Default SSE content type
236
+ format: "sse", // Stream format: "sse" (default) or "nljson"
237
+ contentType: "text/event-stream", // Response content type (auto-set from format)
191
238
  chaos: "low", // Chaos testing level
192
239
  secrets: ["API_KEY"], // Secrets to load
193
240
  setup: [], // Setup function(s)
@@ -296,17 +343,17 @@ for await (const sseEvent of stream.toSSE()) {
296
343
  ### Manual SSE Formatting
297
344
 
298
345
  ```typescript
299
- import { formatSSE } from "jaypie";
346
+ import { formatSse } from "jaypie";
300
347
 
301
348
  const chunk = { type: "message", content: "Hello" };
302
- const sseString = formatSSE(chunk);
349
+ const sseString = formatSse(chunk);
303
350
  // "event: message\ndata: {\"type\":\"message\",\"content\":\"Hello\"}\n\n"
304
351
  ```
305
352
 
306
353
  ### Converting Async Iterables
307
354
 
308
355
  ```typescript
309
- import { streamToSSE } from "jaypie";
356
+ import { streamToSse } from "jaypie";
310
357
 
311
358
  async function* myGenerator() {
312
359
  yield { type: "start", data: {} };
@@ -314,28 +361,36 @@ async function* myGenerator() {
314
361
  yield { type: "end", data: {} };
315
362
  }
316
363
 
317
- for await (const sseEvent of streamToSSE(myGenerator())) {
364
+ for await (const sseEvent of streamToSse(myGenerator())) {
318
365
  responseStream.write(sseEvent);
319
366
  }
320
367
  ```
321
368
 
322
369
  ## Error Handling
323
370
 
324
- Errors in streaming handlers are written as SSE error events:
371
+ Errors in streaming handlers are written to the stream in the configured format:
372
+
373
+ **SSE format (default):**
374
+ ```
375
+ event: error
376
+ data: {"errors":[{"status":500,"title":"Internal Error"}]}
325
377
 
326
- ```typescript
327
- // Format: event: error\ndata: {"errors":[{"status":500,"title":"Internal Error"}]}\n\n
378
+ ```
379
+
380
+ **NLJSON format:**
381
+ ```
382
+ {"error":{"errors":[{"status":500,"title":"Internal Error"}]}}
328
383
  ```
329
384
 
330
385
  For `lambdaStreamHandler`, set `throw: true` to re-throw errors instead of writing to stream:
331
386
 
332
387
  ```typescript
333
- const handler = lambdaStreamHandler(
388
+ export const handler = lambdaStreamHandler(
334
389
  async (event, context) => {
335
390
  throw new InternalError("Something went wrong");
336
391
  },
337
392
  {
338
- throw: true, // Re-throw instead of SSE error
393
+ throw: true, // Re-throw instead of streaming error
339
394
  }
340
395
  );
341
396
  ```
@@ -383,10 +438,13 @@ Use the Docker setup in `packages/express/docker/` for local Lambda streaming te
383
438
  ```typescript
384
439
  // From @jaypie/lambda
385
440
  import type {
441
+ AwsStreamingHandler,
442
+ LambdaHandler, // Wrapped handler type (after awslambda.streamifyResponse)
386
443
  LambdaStreamHandlerOptions,
387
- StreamHandlerContext,
444
+ RawStreamingHandler, // Alias for AwsStreamingHandler (for testing)
388
445
  ResponseStream,
389
- AwsStreamingHandler,
446
+ StreamFormat, // "sse" | "nljson" (re-exported from @jaypie/aws)
447
+ StreamHandlerContext,
390
448
  } from "@jaypie/lambda";
391
449
 
392
450
  // From @jaypie/express (or jaypie)
@@ -402,7 +460,8 @@ import type {
402
460
  import type {
403
461
  ExpressStreamResponse,
404
462
  LambdaStreamWriter,
405
- SSEEvent,
463
+ SseEvent,
406
464
  StreamChunk,
465
+ StreamFormat, // "sse" | "nljson"
407
466
  } from "jaypie";
408
467
  ```