@fresho/router 0.5.0 → 0.6.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/README.md CHANGED
@@ -45,6 +45,7 @@ export default { fetch: api.handler() };
45
45
  - **Typed HTTP client** — call your API with full type safety
46
46
  - **Typed local client** — test handlers directly without HTTP
47
47
  - **OpenAPI generation** — auto-generate docs from your routes
48
+ - **MCP server** — expose routes as tools for AI agents
48
49
  - **Middleware** — cors, auth, rate limiting, and more
49
50
  - **Streaming** — SSE and JSON lines helpers
50
51
  - **HTTP-compliant** — automatic HEAD support for GET routes
@@ -56,6 +57,16 @@ export default { fetch: api.handler() };
56
57
  npm install @fresho/router
57
58
  ```
58
59
 
60
+ ```bash
61
+ bun add @fresho/router
62
+ ```
63
+
64
+ ## Runtime Support
65
+
66
+ The full test suite runs in Node.js 22/24 and Bun 1.3.14. CI also deploys the built
67
+ package to a local Cloudflare `workerd` runtime and verifies routing, validation,
68
+ middleware, and streaming behavior.
69
+
59
70
  ## Path Convention
60
71
 
61
72
  Property names become URL path segments:
@@ -94,6 +105,9 @@ Define query and body schemas using shorthand syntax:
94
105
  { tags: 'string[]' } // string array
95
106
  { scores: 'number[]' } // number array
96
107
 
108
+ // Field descriptions (for OpenAPI and MCP tools)
109
+ { folder: ['string?', 'Mailbox folder, INBOX by default'] }
110
+
97
111
  // Nested objects
98
112
  {
99
113
  address: {
@@ -219,7 +233,7 @@ See **[Middleware Documentation](docs/middleware.md)** for detailed usage.
219
233
  For standalone auth utilities (JWT signing/verification, Basic auth, OAuth), import from `@fresho/router/auth`:
220
234
 
221
235
  ```typescript
222
- import { jwtSign, jwtVerify, parseBasicAuth } from "@fresho/router/auth";
236
+ import { createJwks, jwtSign, jwtVerify, parseBasicAuth } from "@fresho/router/auth";
223
237
 
224
238
  // Sign a JWT
225
239
  const token = await jwtSign({ uid: "user-123" }, secret, { expiresIn: "1h" });
@@ -227,12 +241,20 @@ const token = await jwtSign({ uid: "user-123" }, secret, { expiresIn: "1h" });
227
241
  // Verify a JWT
228
242
  const payload = await jwtVerify(token, secret);
229
243
 
244
+ // Verify a third-party OAuth/OIDC token against the provider's JWKS
245
+ const jwks = createJwks("https://example.auth0.com/.well-known/jwks.json");
246
+ const claims = await jwtVerify(accessToken, jwks, {
247
+ algorithms: ["RS256"],
248
+ issuer: "https://example.auth0.com/",
249
+ audience: "https://api.example.com",
250
+ });
251
+
230
252
  // Parse Basic auth header
231
253
  const creds = parseBasicAuth(request.headers.get("Authorization"));
232
254
  ```
233
255
 
234
256
  Available utilities:
235
- - **JWT**: `jwtSign`, `jwtVerify`
257
+ - **JWT**: `jwtSign`, `jwtVerify`, `createJwks` (HS*, RS*, PS*, ES256/384)
236
258
  - **Basic Auth**: `parseBasicAuth`, `encodeBasicAuth`, `extractBasicAuthToken`
237
259
  - **OAuth**: `encodeOAuthState`, `decodeOAuthState`, `exchangeCode`, `refreshAccessToken`, `revokeToken`, `buildAuthorizationUrl`, `OAUTH_PROVIDERS`
238
260
 
@@ -370,6 +392,52 @@ const docs = router({
370
392
  });
371
393
  ```
372
394
 
395
+ ## MCP Server
396
+
397
+ Expose routes as [Model Context Protocol](https://modelcontextprotocol.io) tools, so agents can use the same API as your other clients. Requires the optional peer dependency `@modelcontextprotocol/sdk`:
398
+
399
+ ```bash
400
+ npm install @modelcontextprotocol/sdk
401
+ ```
402
+
403
+ ```typescript
404
+ import { router } from "@fresho/router";
405
+ import { mcp } from "@fresho/router/mcp";
406
+
407
+ const api = router({
408
+ users,
409
+ billing,
410
+ mcp: mcp({ users }, { name: "my-api", version: "1.0.0" }, bearerAuth({ verify })),
411
+ });
412
+ ```
413
+
414
+ `mcp()` requires endpoint middleware (usually auth). Pass `public: true` instead when the endpoint is open, or already guarded by middleware on its parent routers.
415
+
416
+ - **Tools** — one per route, named from the method and path (`get_users`, `post_users`, `get_users_by_id`, ...) unless the route sets `operationId` (also used in OpenAPI). Descriptions come from `route.description`.
417
+ - **Arguments** — flat: path params, query fields and body fields side by side (`{ id, text }`), as JSON Schema derived from the route's schemas. Names must be unique across the three; a clash throws when `mcp()` is called. Use `[type, description]` tuples (e.g. `{ folder: ['string?', 'Mailbox folder'] }`) in `query` and `body` schemas to document fields for agents and OpenAPI. Array query params are left out, since the router can't parse them from query strings. Without a body schema, `post`/`put`/`patch` tools send any other arguments as the body.
418
+ - **Annotations** — `get` tools are marked `readOnlyHint`, `delete` tools `destructiveHint` and `idempotentHint`, and `put` tools `idempotentHint`, so clients can decide when to ask for confirmation. Override per route with `mcp: { annotations: { ... } }`.
419
+ - **Calls** — each tool call is dispatched as an HTTP request through the given routers with the caller's headers forwarded. Middleware on the MCP endpoint (passed to `mcp()` or on its parent routers) runs once per MCP request, and what it adds to the context (e.g. `c.user`) reaches handlers; middleware on the given routers and below runs on every call. Middleware on the given routers' *own* parents elsewhere in your app does **not** run, so guard the endpoint or the routers themselves. Responses become text content, plus `structuredContent` when the body is a JSON object; a non-2xx status sets `isError`, and streaming (SSE, NDJSON) responses are rejected.
420
+ - **Safety** — requests with an `Origin` other than the endpoint's get 403 (as the MCP spec requires against DNS rebinding), so browser-hosted clients on other origins can't connect. Path parameters can't be `.` or `..`, and handler exceptions become a generic `Internal error`.
421
+ - **Transport** — stateless Streamable HTTP with JSON responses; `GET` and `DELETE` return 405.
422
+ - Pass a router to expose all of its routes, or a record of routers to mount them under their keys.
423
+
424
+ ### OAuth
425
+
426
+ Put auth middleware on the endpoint so unauthenticated clients get an HTTP 401, and set `oauth` to advertise your authorization server (RFC 9728):
427
+
428
+ ```typescript
429
+ const oauth = { authorizationServers: ["https://auth.example.com"], scopes: ["users:read"] };
430
+
431
+ const app = router({
432
+ users,
433
+ mcp: mcp({ users }, { name: "my-api", version: "1.0.0", oauth }, jwtAuth({ ... })),
434
+ // Optional: metadata at the origin root, for clients that probe it before calling the endpoint.
435
+ ".well-known": oauthProtectedResource(oauth),
436
+ });
437
+ ```
438
+
439
+ 401s from the endpoint gain `WWW-Authenticate: Bearer resource_metadata="https://host/mcp/.well-known/oauth-protected-resource"`, and that URL serves the metadata without the endpoint's middleware (keep auth off the endpoint's parent routers, or the metadata is guarded too). `oauthProtectedResource` only handles single-segment endpoint paths such as `/mcp`. Validate that tokens were issued for this server (the `aud` claim), not just by your authorization server.
440
+
373
441
  ## Streaming
374
442
 
375
443
  ### Server-Sent Events
@@ -1,4 +1,4 @@
1
- import { a as VerifyJwtOptions, i as SignJwtOptions, n as JwtPayload, o as jwtSign, r as JwtSecret, s as jwtVerify, t as JwtAlgorithm } from "../jwt-DHM_nSwL.mjs";
1
+ import { a as SignJwtOptions, c as jwtSign, i as JwtSecret, l as jwtVerify, n as JwtAlgorithm, o as VerifyJwtOptions, r as JwtPayload, s as createJwks, t as Jwks } from "../jwt-B-Fqwu4E.mjs";
2
2
 
3
3
  //#region src/auth/basic.d.ts
4
4
 
@@ -323,5 +323,5 @@ interface AuthorizationUrlOptions {
323
323
  */
324
324
  declare function buildAuthorizationUrl(options: AuthorizationUrlOptions): string;
325
325
  //#endregion
326
- export { type AuthorizationUrlOptions, type BasicCredentials, type ExchangeCodeOptions, type JwtAlgorithm, type JwtPayload, type JwtSecret, OAUTH_PROVIDERS, type OAuthProvider, type OAuthTokenResponse, type RevokeTokenOptions, type SignJwtOptions, type TokenAuthMethod, type VerifyJwtOptions, buildAuthorizationUrl, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
326
+ export { type AuthorizationUrlOptions, type BasicCredentials, type ExchangeCodeOptions, type Jwks, type JwtAlgorithm, type JwtPayload, type JwtSecret, OAUTH_PROVIDERS, type OAuthProvider, type OAuthTokenResponse, type RevokeTokenOptions, type SignJwtOptions, type TokenAuthMethod, type VerifyJwtOptions, buildAuthorizationUrl, createJwks, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
327
327
  //# sourceMappingURL=index.d.mts.map
@@ -1,4 +1,4 @@
1
- import { a as parseBasicAuth, i as extractBasicAuthToken, n as jwtVerify, r as encodeBasicAuth, t as jwtSign } from "../jwt-Dab9kOC2.mjs";
1
+ import { a as extractBasicAuthToken, i as encodeBasicAuth, n as jwtSign, o as parseBasicAuth, r as jwtVerify, t as createJwks } from "../jwt-ikNuUBcp.mjs";
2
2
 
3
3
  //#region src/auth/oauth.ts
4
4
  /** Common OAuth 2.0 providers with their endpoints. */
@@ -275,5 +275,5 @@ function buildAuthorizationUrl(options) {
275
275
  }
276
276
 
277
277
  //#endregion
278
- export { OAUTH_PROVIDERS, buildAuthorizationUrl, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
278
+ export { OAUTH_PROVIDERS, buildAuthorizationUrl, createJwks, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
279
279
  //# sourceMappingURL=index.mjs.map