@jaypie/mcp 0.2.10 → 0.3.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 +62 -0
- package/dist/aws.d.ts +197 -0
- package/dist/index.js +1179 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/prompts/Jaypie_CDK_Constructs_and_Patterns.md +23 -28
- package/prompts/Jaypie_CICD_with_GitHub_Actions.md +141 -14
- package/prompts/Jaypie_DynamoDB_Package.md +55 -55
- package/prompts/Jaypie_Express_Package.md +0 -44
- package/prompts/{Jaypie_Vocabulary_Commander.md → Jaypie_Fabric_Commander.md} +38 -38
- package/prompts/{Jaypie_Vocabulary_LLM.md → Jaypie_Fabric_LLM.md} +45 -45
- package/prompts/{Jaypie_Vocabulary_Lambda.md → Jaypie_Fabric_Lambda.md} +40 -42
- package/prompts/{Jaypie_Vocabulary_MCP.md → Jaypie_Fabric_MCP.md} +39 -39
- package/prompts/{Jaypie_Vocabulary_Package.md → Jaypie_Fabric_Package.md} +151 -142
- package/prompts/Jaypie_Init_CICD_with_GitHub_Actions.md +151 -104
- package/prompts/Jaypie_MCP_Package.md +249 -0
- package/prompts/Jaypie_Streaming.md +408 -0
|
@@ -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
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Complete guide to streaming in Jaypie - Lambda, Express, and SSE patterns
|
|
3
|
+
globs: packages/express/**, packages/lambda/**, packages/aws/**
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Jaypie Streaming Guide
|
|
7
|
+
|
|
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
|
+
|
|
10
|
+
## Quick Reference
|
|
11
|
+
|
|
12
|
+
| Handler | Package | Express Required | Use Case |
|
|
13
|
+
|---------|---------|------------------|----------|
|
|
14
|
+
| `lambdaStreamHandler` | `@jaypie/lambda` | No | Pure Lambda Function URL streaming |
|
|
15
|
+
| `expressStreamHandler` | `@jaypie/express` | Yes | Express routes with SSE |
|
|
16
|
+
| `createLambdaStreamHandler` | `@jaypie/express` | Yes | Express app deployed to Lambda with streaming |
|
|
17
|
+
|
|
18
|
+
### Decision Guide
|
|
19
|
+
|
|
20
|
+
- **Building a pure Lambda function?** Use `lambdaStreamHandler`
|
|
21
|
+
- **Building an Express app for non-Lambda deployment?** Use `expressStreamHandler`
|
|
22
|
+
- **Building an Express app that runs on Lambda?** Use `createLambdaStreamHandler`
|
|
23
|
+
|
|
24
|
+
## Streaming Utilities
|
|
25
|
+
|
|
26
|
+
All streaming utilities are exported from `@jaypie/aws` and re-exported through `jaypie`:
|
|
27
|
+
|
|
28
|
+
| Function | Purpose |
|
|
29
|
+
|----------|---------|
|
|
30
|
+
| `createLambdaStream(stream, writer)` | Pipe async iterable to Lambda response writer |
|
|
31
|
+
| `createExpressStream(stream, res)` | Pipe async iterable to Express response with SSE headers |
|
|
32
|
+
| `JaypieStream` | Wrapper class with `.toLambda()` and `.toExpress()` methods |
|
|
33
|
+
| `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 |
|
|
36
|
+
|
|
37
|
+
### Types
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import type {
|
|
41
|
+
ExpressStreamResponse,
|
|
42
|
+
LambdaStreamWriter,
|
|
43
|
+
SSEEvent,
|
|
44
|
+
StreamChunk,
|
|
45
|
+
} from "jaypie";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Pattern 1: lambdaStreamHandler
|
|
49
|
+
|
|
50
|
+
Use for pure Lambda functions without Express. Requires AWS Lambda Response Streaming via Function URL.
|
|
51
|
+
|
|
52
|
+
### Basic Usage
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { lambdaStreamHandler } from "jaypie";
|
|
56
|
+
import type { StreamHandlerContext } from "@jaypie/lambda";
|
|
57
|
+
|
|
58
|
+
const streamWorker = lambdaStreamHandler(
|
|
59
|
+
async (event: unknown, context: StreamHandlerContext) => {
|
|
60
|
+
const { responseStream } = context;
|
|
61
|
+
|
|
62
|
+
responseStream.write("event: start\ndata: {}\n\n");
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < 5; i++) {
|
|
65
|
+
responseStream.write(`event: data\ndata: {"count": ${i}}\n\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
responseStream.write("event: done\ndata: {}\n\n");
|
|
69
|
+
// Handler automatically calls responseStream.end()
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "streamWorker",
|
|
73
|
+
contentType: "text/event-stream",
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Wrap with AWS streamifyResponse for Lambda
|
|
78
|
+
declare const awslambda: { streamifyResponse: <T>(handler: T) => T };
|
|
79
|
+
export const handler = awslambda.streamifyResponse(streamWorker);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### With LLM Streaming
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { lambdaStreamHandler, createLambdaStream, Llm } from "jaypie";
|
|
86
|
+
import type { StreamHandlerContext } from "@jaypie/lambda";
|
|
87
|
+
|
|
88
|
+
interface PromptEvent {
|
|
89
|
+
prompt?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const llmStreamHandler = lambdaStreamHandler(
|
|
93
|
+
async (event: PromptEvent, context: StreamHandlerContext) => {
|
|
94
|
+
const llm = new Llm("anthropic");
|
|
95
|
+
const stream = llm.stream(event.prompt || "Hello");
|
|
96
|
+
|
|
97
|
+
// createLambdaStream pipes chunks as SSE events
|
|
98
|
+
await createLambdaStream(stream, context.responseStream);
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "llmStream",
|
|
102
|
+
secrets: ["ANTHROPIC_API_KEY"],
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
declare const awslambda: { streamifyResponse: <T>(handler: T) => T };
|
|
107
|
+
export const handler = awslambda.streamifyResponse(llmStreamHandler);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Handler Options
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import type { LambdaStreamHandlerOptions } from "@jaypie/lambda";
|
|
114
|
+
|
|
115
|
+
const options: LambdaStreamHandlerOptions = {
|
|
116
|
+
name: "myStreamHandler", // Handler name for logging
|
|
117
|
+
contentType: "text/event-stream", // Response content type (default)
|
|
118
|
+
chaos: "low", // Chaos testing level
|
|
119
|
+
secrets: ["API_KEY"], // AWS secrets to load into process.env
|
|
120
|
+
setup: [], // Setup function(s)
|
|
121
|
+
teardown: [], // Teardown function(s)
|
|
122
|
+
validate: [], // Validation function(s)
|
|
123
|
+
throw: false, // Re-throw errors instead of SSE error
|
|
124
|
+
unavailable: false, // Return 503 if true
|
|
125
|
+
};
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### CDK Configuration
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { JaypieLambda } from "@jaypie/constructs";
|
|
132
|
+
import { FunctionUrlAuthType, InvokeMode } from "aws-cdk-lib/aws-lambda";
|
|
133
|
+
|
|
134
|
+
const streamingLambda = new JaypieLambda(this, "StreamingFunction", {
|
|
135
|
+
code: "dist",
|
|
136
|
+
handler: "streamWorker.handler",
|
|
137
|
+
timeout: Duration.minutes(5),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Enable Lambda Response Streaming
|
|
141
|
+
streamingLambda.addFunctionUrl({
|
|
142
|
+
authType: FunctionUrlAuthType.NONE,
|
|
143
|
+
invokeMode: InvokeMode.RESPONSE_STREAM,
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Pattern 2: expressStreamHandler
|
|
148
|
+
|
|
149
|
+
Use for Express applications not running on Lambda. Sets SSE headers automatically.
|
|
150
|
+
|
|
151
|
+
### Basic Usage
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { expressStreamHandler } from "jaypie";
|
|
155
|
+
import type { Request, Response } from "express";
|
|
156
|
+
|
|
157
|
+
const streamRoute = expressStreamHandler(async (req: Request, res: Response) => {
|
|
158
|
+
// Write SSE events directly to response
|
|
159
|
+
res.write("event: message\ndata: {\"text\": \"Hello\"}\n\n");
|
|
160
|
+
res.write("event: message\ndata: {\"text\": \"World\"}\n\n");
|
|
161
|
+
// Handler automatically ends the stream
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
app.get("/stream", streamRoute);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### With LLM Streaming
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { expressStreamHandler, createExpressStream, Llm } from "jaypie";
|
|
171
|
+
|
|
172
|
+
const llmStreamRoute = expressStreamHandler(async (req: Request, res: Response) => {
|
|
173
|
+
const llm = new Llm("anthropic");
|
|
174
|
+
const stream = llm.stream(req.body.prompt);
|
|
175
|
+
|
|
176
|
+
// createExpressStream pipes LLM chunks as SSE events
|
|
177
|
+
await createExpressStream(stream, res);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
app.post("/chat", llmStreamRoute);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Handler Options
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import type { ExpressStreamHandlerOptions } from "jaypie";
|
|
187
|
+
|
|
188
|
+
const options: ExpressStreamHandlerOptions = {
|
|
189
|
+
name: "myStreamHandler", // Handler name for logging
|
|
190
|
+
contentType: "text/event-stream", // Default SSE content type
|
|
191
|
+
chaos: "low", // Chaos testing level
|
|
192
|
+
secrets: ["API_KEY"], // Secrets to load
|
|
193
|
+
setup: [], // Setup function(s)
|
|
194
|
+
teardown: [], // Teardown function(s)
|
|
195
|
+
validate: [], // Validation function(s)
|
|
196
|
+
locals: {}, // Values to set on req.locals
|
|
197
|
+
unavailable: false, // Return 503 if true
|
|
198
|
+
};
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### SSE Headers
|
|
202
|
+
|
|
203
|
+
`expressStreamHandler` automatically sets these headers:
|
|
204
|
+
|
|
205
|
+
- `Content-Type: text/event-stream`
|
|
206
|
+
- `Cache-Control: no-cache`
|
|
207
|
+
- `Connection: keep-alive`
|
|
208
|
+
- `X-Accel-Buffering: no` (disables nginx buffering)
|
|
209
|
+
|
|
210
|
+
## Pattern 3: createLambdaStreamHandler
|
|
211
|
+
|
|
212
|
+
Use for Express applications deployed to AWS Lambda with streaming support.
|
|
213
|
+
|
|
214
|
+
### Basic Usage
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import express from "express";
|
|
218
|
+
import { createLambdaStreamHandler, expressStreamHandler } from "jaypie";
|
|
219
|
+
|
|
220
|
+
const app = express();
|
|
221
|
+
|
|
222
|
+
app.get("/stream", expressStreamHandler(async (req, res) => {
|
|
223
|
+
res.write("event: message\ndata: {\"text\": \"Hello\"}\n\n");
|
|
224
|
+
res.write("event: message\ndata: {\"text\": \"World\"}\n\n");
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
// Export streaming Lambda handler for Express app
|
|
228
|
+
export const handler = createLambdaStreamHandler(app);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Combined Buffered and Streaming
|
|
232
|
+
|
|
233
|
+
When you need both regular endpoints and streaming endpoints:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import express from "express";
|
|
237
|
+
import {
|
|
238
|
+
createLambdaHandler,
|
|
239
|
+
createLambdaStreamHandler,
|
|
240
|
+
expressHandler,
|
|
241
|
+
expressStreamHandler,
|
|
242
|
+
cors,
|
|
243
|
+
} from "jaypie";
|
|
244
|
+
|
|
245
|
+
const app = express();
|
|
246
|
+
app.use(express.json());
|
|
247
|
+
app.use(cors());
|
|
248
|
+
|
|
249
|
+
// Standard buffered route
|
|
250
|
+
app.get("/api/data", expressHandler(async (req, res) => {
|
|
251
|
+
return { data: "buffered response" };
|
|
252
|
+
}));
|
|
253
|
+
|
|
254
|
+
// SSE streaming route
|
|
255
|
+
app.get("/api/stream", expressStreamHandler(async (req, res) => {
|
|
256
|
+
for (let i = 0; i < 5; i++) {
|
|
257
|
+
res.write(`event: update\ndata: {"count": ${i}}\n\n`);
|
|
258
|
+
}
|
|
259
|
+
}));
|
|
260
|
+
|
|
261
|
+
// Choose based on needs:
|
|
262
|
+
// - createLambdaHandler for buffered-only
|
|
263
|
+
// - createLambdaStreamHandler for streaming support
|
|
264
|
+
export const handler = createLambdaStreamHandler(app);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Streaming Utilities Deep Dive
|
|
268
|
+
|
|
269
|
+
### JaypieStream Class
|
|
270
|
+
|
|
271
|
+
Wraps an async iterable for convenient streaming to different targets:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { JaypieStream, createJaypieStream, Llm } from "jaypie";
|
|
275
|
+
|
|
276
|
+
const llm = new Llm("anthropic");
|
|
277
|
+
const stream = createJaypieStream(llm.stream("Hello"));
|
|
278
|
+
|
|
279
|
+
// Pipe to Lambda
|
|
280
|
+
await stream.toLambda(context.responseStream);
|
|
281
|
+
|
|
282
|
+
// Or pipe to Express
|
|
283
|
+
await stream.toExpress(res);
|
|
284
|
+
|
|
285
|
+
// Or iterate directly
|
|
286
|
+
for await (const chunk of stream) {
|
|
287
|
+
console.log(chunk);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Or convert to SSE strings
|
|
291
|
+
for await (const sseEvent of stream.toSSE()) {
|
|
292
|
+
console.log(sseEvent);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Manual SSE Formatting
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
import { formatSSE } from "jaypie";
|
|
300
|
+
|
|
301
|
+
const chunk = { type: "message", content: "Hello" };
|
|
302
|
+
const sseString = formatSSE(chunk);
|
|
303
|
+
// "event: message\ndata: {\"type\":\"message\",\"content\":\"Hello\"}\n\n"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Converting Async Iterables
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
import { streamToSSE } from "jaypie";
|
|
310
|
+
|
|
311
|
+
async function* myGenerator() {
|
|
312
|
+
yield { type: "start", data: {} };
|
|
313
|
+
yield { type: "data", value: 42 };
|
|
314
|
+
yield { type: "end", data: {} };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
for await (const sseEvent of streamToSSE(myGenerator())) {
|
|
318
|
+
responseStream.write(sseEvent);
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Error Handling
|
|
323
|
+
|
|
324
|
+
Errors in streaming handlers are written as SSE error events:
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// Format: event: error\ndata: {"errors":[{"status":500,"title":"Internal Error"}]}\n\n
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
For `lambdaStreamHandler`, set `throw: true` to re-throw errors instead of writing to stream:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
const handler = lambdaStreamHandler(
|
|
334
|
+
async (event, context) => {
|
|
335
|
+
throw new InternalError("Something went wrong");
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
throw: true, // Re-throw instead of SSE error
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Testing Streaming Handlers
|
|
344
|
+
|
|
345
|
+
### Mocking for Unit Tests
|
|
346
|
+
|
|
347
|
+
```typescript
|
|
348
|
+
import { describe, expect, it, vi } from "vitest";
|
|
349
|
+
|
|
350
|
+
vi.mock("jaypie", async () => {
|
|
351
|
+
const testkit = await import("@jaypie/testkit/mock");
|
|
352
|
+
return testkit;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
import { handler } from "./streamWorker.js";
|
|
356
|
+
|
|
357
|
+
describe("Stream Handler", () => {
|
|
358
|
+
it("writes expected events", async () => {
|
|
359
|
+
const writes: string[] = [];
|
|
360
|
+
const mockWriter = {
|
|
361
|
+
write: vi.fn((chunk) => writes.push(chunk)),
|
|
362
|
+
end: vi.fn(),
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const event = { prompt: "test" };
|
|
366
|
+
const context = { responseStream: mockWriter };
|
|
367
|
+
|
|
368
|
+
await handler(event, context);
|
|
369
|
+
|
|
370
|
+
expect(mockWriter.write).toHaveBeenCalled();
|
|
371
|
+
expect(writes.some(w => w.includes("event:"))).toBe(true);
|
|
372
|
+
expect(mockWriter.end).toHaveBeenCalled();
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Integration Testing
|
|
378
|
+
|
|
379
|
+
Use the Docker setup in `packages/express/docker/` for local Lambda streaming tests.
|
|
380
|
+
|
|
381
|
+
## TypeScript Types
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// From @jaypie/lambda
|
|
385
|
+
import type {
|
|
386
|
+
LambdaStreamHandlerOptions,
|
|
387
|
+
StreamHandlerContext,
|
|
388
|
+
ResponseStream,
|
|
389
|
+
AwsStreamingHandler,
|
|
390
|
+
} from "@jaypie/lambda";
|
|
391
|
+
|
|
392
|
+
// From @jaypie/express (or jaypie)
|
|
393
|
+
import type {
|
|
394
|
+
ExpressStreamHandlerOptions,
|
|
395
|
+
ExpressStreamHandlerLocals,
|
|
396
|
+
JaypieStreamHandlerSetup,
|
|
397
|
+
JaypieStreamHandlerTeardown,
|
|
398
|
+
JaypieStreamHandlerValidate,
|
|
399
|
+
} from "jaypie";
|
|
400
|
+
|
|
401
|
+
// From @jaypie/aws (or jaypie)
|
|
402
|
+
import type {
|
|
403
|
+
ExpressStreamResponse,
|
|
404
|
+
LambdaStreamWriter,
|
|
405
|
+
SSEEvent,
|
|
406
|
+
StreamChunk,
|
|
407
|
+
} from "jaypie";
|
|
408
|
+
```
|