@jaypie/mcp 0.1.8 → 0.1.10
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/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Finlayson Studio, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jaypie/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Jaypie MCP",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
38
38
|
"commander": "^14.0.0",
|
|
39
39
|
"gray-matter": "^4.0.3",
|
|
40
|
-
"zod": "^
|
|
40
|
+
"zod": "^4.1.13"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/express": "^5.0.3",
|
|
@@ -45,5 +45,6 @@
|
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"access": "public"
|
|
48
|
-
}
|
|
48
|
+
},
|
|
49
|
+
"gitHead": "45fe048c6687a807ed52bbc0b25963f45450f9c9"
|
|
49
50
|
}
|
|
@@ -94,6 +94,11 @@ new JaypieInfrastructureStack(scope, "InfraStack"); // Infrastructure resources
|
|
|
94
94
|
|
|
95
95
|
Use `JaypieEnvSecret` for cross-stack secret sharing:
|
|
96
96
|
```typescript
|
|
97
|
+
// Shorthand: if API_KEY exists in process.env, uses it as envKey
|
|
98
|
+
// Creates construct with id "EnvSecret_API_KEY"
|
|
99
|
+
new JaypieEnvSecret(this, "API_KEY");
|
|
100
|
+
|
|
101
|
+
// Explicit configuration
|
|
97
102
|
new JaypieEnvSecret(this, "ApiKey", {
|
|
98
103
|
envKey: "API_KEY",
|
|
99
104
|
provider: true, // Exports for other stacks
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Complete guide to using Jaypie Express features including expressHandler, CORS, lifecycle hooks, and pre-built routes
|
|
3
|
+
globs: packages/express/**
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Jaypie Express Package
|
|
7
|
+
|
|
8
|
+
Jaypie provides Express utilities through `@jaypie/express` (also available via `jaypie`). The primary export is `expressHandler`, a wrapper that adds error handling, logging, lifecycle hooks, and response formatting to Express route handlers.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install jaypie
|
|
14
|
+
# or
|
|
15
|
+
npm install @jaypie/express
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## expressHandler
|
|
19
|
+
|
|
20
|
+
The core of Jaypie Express. Wraps route handlers with error handling, logging, and lifecycle management.
|
|
21
|
+
|
|
22
|
+
### Basic Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { expressHandler } from "jaypie";
|
|
26
|
+
import type { Request, Response } from "express";
|
|
27
|
+
|
|
28
|
+
const myRoute = expressHandler(async (req: Request, res: Response) => {
|
|
29
|
+
return { message: "Hello, World!" };
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Use in Express
|
|
33
|
+
app.get("/hello", myRoute);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Return Value Handling
|
|
37
|
+
|
|
38
|
+
expressHandler automatically formats responses:
|
|
39
|
+
|
|
40
|
+
| Return Value | HTTP Status | Response |
|
|
41
|
+
|--------------|-------------|----------|
|
|
42
|
+
| Object | 200 | JSON body |
|
|
43
|
+
| Array | 200 | JSON body |
|
|
44
|
+
| String (JSON) | 200 | Parsed JSON |
|
|
45
|
+
| String (other) | 200 | Text body |
|
|
46
|
+
| `true` | 201 Created | Empty |
|
|
47
|
+
| `null`, `undefined`, `false` | 204 No Content | Empty |
|
|
48
|
+
| Object with `.json()` method | 200 | Result of `.json()` |
|
|
49
|
+
|
|
50
|
+
### Options
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { expressHandler } from "jaypie";
|
|
54
|
+
import type { ExpressHandlerOptions } from "jaypie";
|
|
55
|
+
|
|
56
|
+
const options: ExpressHandlerOptions = {
|
|
57
|
+
name: "myHandler", // Handler name for logging
|
|
58
|
+
chaos: "low", // Chaos testing level
|
|
59
|
+
unavailable: false, // Return 503 if true
|
|
60
|
+
setup: [], // Setup function(s)
|
|
61
|
+
teardown: [], // Teardown function(s)
|
|
62
|
+
validate: [], // Validation function(s)
|
|
63
|
+
locals: {}, // Values to set on req.locals
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handler = expressHandler(async (req, res) => {
|
|
67
|
+
return { success: true };
|
|
68
|
+
}, options);
|
|
69
|
+
|
|
70
|
+
// Alternative: options first
|
|
71
|
+
const handler2 = expressHandler(options, async (req, res) => {
|
|
72
|
+
return { success: true };
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Lifecycle Hooks
|
|
77
|
+
|
|
78
|
+
### Setup Functions
|
|
79
|
+
|
|
80
|
+
Run before the main handler. Use for initialization, authentication checks, or setting up request context.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { expressHandler } from "jaypie";
|
|
84
|
+
import type { JaypieHandlerSetup } from "jaypie";
|
|
85
|
+
|
|
86
|
+
const authenticateUser: JaypieHandlerSetup = async (req, res) => {
|
|
87
|
+
const token = req.headers.authorization;
|
|
88
|
+
// Validate token, throw UnauthorizedError if invalid
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const loadTenant: JaypieHandlerSetup = async (req, res) => {
|
|
92
|
+
req.locals.tenant = await getTenant(req.params.tenantId);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handler = expressHandler(
|
|
96
|
+
async (req, res) => {
|
|
97
|
+
// req.locals.tenant is available here
|
|
98
|
+
return { tenant: req.locals.tenant };
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
setup: [authenticateUser, loadTenant],
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Teardown Functions
|
|
107
|
+
|
|
108
|
+
Run after the main handler completes (success or error). Use for cleanup.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import type { JaypieHandlerTeardown } from "jaypie";
|
|
112
|
+
|
|
113
|
+
const closeConnection: JaypieHandlerTeardown = async (req, res) => {
|
|
114
|
+
await req.locals.dbConnection?.close();
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handler = expressHandler(
|
|
118
|
+
async (req, res) => {
|
|
119
|
+
req.locals.dbConnection = await openConnection();
|
|
120
|
+
return await doWork(req.locals.dbConnection);
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
teardown: closeConnection,
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Validation Functions
|
|
129
|
+
|
|
130
|
+
Run before the main handler. Return `true` to continue or `false`/throw to reject.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { ForbiddenError } from "jaypie";
|
|
134
|
+
import type { JaypieHandlerValidate } from "jaypie";
|
|
135
|
+
|
|
136
|
+
const requireAdmin: JaypieHandlerValidate = (req, res) => {
|
|
137
|
+
if (!req.locals.user?.isAdmin) {
|
|
138
|
+
throw new ForbiddenError();
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const validateBody: JaypieHandlerValidate = (req, res) => {
|
|
144
|
+
return req.body?.email && req.body?.name;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const handler = expressHandler(
|
|
148
|
+
async (req, res) => {
|
|
149
|
+
// Only runs if user is admin and body is valid
|
|
150
|
+
return { success: true };
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
validate: [requireAdmin, validateBody],
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Locals
|
|
159
|
+
|
|
160
|
+
Set values on `req.locals`. Values can be static or functions that receive `(req, res)`.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
import type { ExpressHandlerLocals } from "jaypie";
|
|
164
|
+
|
|
165
|
+
const getUser: ExpressHandlerLocals = async (req, res) => {
|
|
166
|
+
return await User.findById(req.params.userId);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const handler = expressHandler(
|
|
170
|
+
async (req, res) => {
|
|
171
|
+
// Access via req.locals
|
|
172
|
+
console.log(req.locals.apiVersion); // "v1"
|
|
173
|
+
console.log(req.locals.user); // User object
|
|
174
|
+
return req.locals.user;
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
locals: {
|
|
178
|
+
apiVersion: "v1", // Static value
|
|
179
|
+
user: getUser, // Function called during setup
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## CORS Helper
|
|
186
|
+
|
|
187
|
+
Configure CORS middleware with Jaypie conventions.
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
import { cors } from "jaypie";
|
|
191
|
+
import type { CorsConfig } from "jaypie";
|
|
192
|
+
|
|
193
|
+
// Default: uses BASE_URL or PROJECT_BASE_URL env vars
|
|
194
|
+
app.use(cors());
|
|
195
|
+
|
|
196
|
+
// Wildcard origin
|
|
197
|
+
app.use(cors({ origin: "*" }));
|
|
198
|
+
|
|
199
|
+
// Specific origin
|
|
200
|
+
app.use(cors({ origin: "https://example.com" }));
|
|
201
|
+
|
|
202
|
+
// Custom configuration
|
|
203
|
+
const corsConfig: CorsConfig = {
|
|
204
|
+
origin: "https://api.example.com",
|
|
205
|
+
// Additional cors options
|
|
206
|
+
};
|
|
207
|
+
app.use(cors(corsConfig));
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Environment variables:
|
|
211
|
+
- `BASE_URL` or `PROJECT_BASE_URL`: Default allowed origin
|
|
212
|
+
- `PROJECT_ENV=sandbox` or `PROJECT_SANDBOX_MODE=true`: Allows localhost
|
|
213
|
+
|
|
214
|
+
## Pre-built Routes
|
|
215
|
+
|
|
216
|
+
Ready-to-use route handlers for common responses.
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
import {
|
|
220
|
+
badRequestRoute, // 400 Bad Request
|
|
221
|
+
echoRoute, // 200 with request echo
|
|
222
|
+
forbiddenRoute, // 403 Forbidden
|
|
223
|
+
goneRoute, // 410 Gone
|
|
224
|
+
methodNotAllowedRoute, // 405 Method Not Allowed
|
|
225
|
+
noContentRoute, // 204 No Content
|
|
226
|
+
notFoundRoute, // 404 Not Found
|
|
227
|
+
notImplementedRoute, // 501 Not Implemented
|
|
228
|
+
} from "jaypie";
|
|
229
|
+
|
|
230
|
+
// Use as catch-all or placeholder routes
|
|
231
|
+
app.all("/deprecated/*", goneRoute);
|
|
232
|
+
app.use("*", notFoundRoute);
|
|
233
|
+
|
|
234
|
+
// Echo route for debugging
|
|
235
|
+
app.get("/debug/echo", echoRoute);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## HTTP Code Handler
|
|
239
|
+
|
|
240
|
+
Create custom HTTP status code handlers.
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { expressHttpCodeHandler, HTTP } from "jaypie";
|
|
244
|
+
|
|
245
|
+
// Returns 200 OK with empty body
|
|
246
|
+
const okRoute = expressHttpCodeHandler(HTTP.CODE.OK);
|
|
247
|
+
|
|
248
|
+
// Returns 202 Accepted
|
|
249
|
+
const acceptedRoute = expressHttpCodeHandler(202, { name: "accepted" });
|
|
250
|
+
|
|
251
|
+
app.post("/jobs", acceptedRoute);
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Error Handling
|
|
255
|
+
|
|
256
|
+
Throw Jaypie errors for proper HTTP responses.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import {
|
|
260
|
+
expressHandler,
|
|
261
|
+
BadRequestError,
|
|
262
|
+
NotFoundError,
|
|
263
|
+
UnauthorizedError,
|
|
264
|
+
ForbiddenError,
|
|
265
|
+
InternalError,
|
|
266
|
+
log,
|
|
267
|
+
} from "jaypie";
|
|
268
|
+
|
|
269
|
+
const handler = expressHandler(async (req, res) => {
|
|
270
|
+
const item = await findItem(req.params.id);
|
|
271
|
+
|
|
272
|
+
if (!item) {
|
|
273
|
+
log.warn("Item not found");
|
|
274
|
+
throw new NotFoundError();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!canAccess(req.user, item)) {
|
|
278
|
+
throw new ForbiddenError();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return item;
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Errors return JSON:API compliant error responses:
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"errors": [{
|
|
290
|
+
"status": 404,
|
|
291
|
+
"title": "Not Found",
|
|
292
|
+
"detail": "The requested resource was not found"
|
|
293
|
+
}]
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## TypeScript Types
|
|
298
|
+
|
|
299
|
+
All lifecycle function types are exported for type safety:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import type {
|
|
303
|
+
ExpressHandlerOptions,
|
|
304
|
+
ExpressHandlerLocals,
|
|
305
|
+
JaypieHandlerSetup,
|
|
306
|
+
JaypieHandlerTeardown,
|
|
307
|
+
JaypieHandlerValidate,
|
|
308
|
+
CorsConfig,
|
|
309
|
+
} from "jaypie";
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Complete Example
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import express from "express";
|
|
316
|
+
import {
|
|
317
|
+
cors,
|
|
318
|
+
expressHandler,
|
|
319
|
+
notFoundRoute,
|
|
320
|
+
NotFoundError,
|
|
321
|
+
ForbiddenError,
|
|
322
|
+
log,
|
|
323
|
+
} from "jaypie";
|
|
324
|
+
import type {
|
|
325
|
+
JaypieHandlerSetup,
|
|
326
|
+
JaypieHandlerValidate,
|
|
327
|
+
ExpressHandlerLocals,
|
|
328
|
+
} from "jaypie";
|
|
329
|
+
|
|
330
|
+
const app = express();
|
|
331
|
+
app.use(express.json());
|
|
332
|
+
app.use(cors());
|
|
333
|
+
|
|
334
|
+
// Lifecycle functions
|
|
335
|
+
const authenticate: JaypieHandlerSetup = async (req, res) => {
|
|
336
|
+
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
337
|
+
if (!token) throw new UnauthorizedError();
|
|
338
|
+
req.locals.user = await verifyToken(token);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const requireOwner: JaypieHandlerValidate = (req, res) => {
|
|
342
|
+
return req.locals.resource?.ownerId === req.locals.user?.id;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const loadResource: ExpressHandlerLocals = async (req, res) => {
|
|
346
|
+
const resource = await Resource.findById(req.params.id);
|
|
347
|
+
if (!resource) throw new NotFoundError();
|
|
348
|
+
return resource;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// Route handler
|
|
352
|
+
const updateResource = expressHandler(
|
|
353
|
+
async (req, res) => {
|
|
354
|
+
const { resource, user } = req.locals;
|
|
355
|
+
|
|
356
|
+
resource.name = req.body.name;
|
|
357
|
+
resource.updatedBy = user.id;
|
|
358
|
+
await resource.save();
|
|
359
|
+
|
|
360
|
+
log.trace("Resource updated");
|
|
361
|
+
return resource;
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "updateResource",
|
|
365
|
+
setup: authenticate,
|
|
366
|
+
validate: requireOwner,
|
|
367
|
+
locals: {
|
|
368
|
+
resource: loadResource,
|
|
369
|
+
},
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
app.put("/resources/:id", updateResource);
|
|
374
|
+
app.use("*", notFoundRoute);
|
|
375
|
+
|
|
376
|
+
export default app;
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Response Headers
|
|
380
|
+
|
|
381
|
+
expressHandler automatically sets:
|
|
382
|
+
- `X-Powered-By: @jaypie/express`
|
|
383
|
+
- `X-Project-Handler: {name}` (if name option provided)
|
|
384
|
+
- `X-Project-Invocation: {uuid}` (request tracking ID)
|
|
385
|
+
|
|
386
|
+
## Datadog Integration
|
|
387
|
+
|
|
388
|
+
When Datadog environment variables are configured, expressHandler automatically submits metrics for each request including status code and path.
|