keryx 0.11.2 → 0.11.4

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 ADDED
@@ -0,0 +1,225 @@
1
+ # Keryx
2
+
3
+ <p align="center"><strong>The fullstack TypeScript framework for MCP and APIs.</strong></p>
4
+
5
+ <p align="center">
6
+ <img src="https://raw.githubusercontent.com/evantahler/keryx/main/docs/public/images/horn.svg" alt="Keryx" width="200" />
7
+ </p>
8
+
9
+ [![Test](https://github.com/evantahler/keryx/actions/workflows/test.yaml/badge.svg)](https://github.com/evantahler/keryx/actions/workflows/test.yaml)
10
+
11
+ ## What is this Project?
12
+
13
+ This is a ground-up rewrite of [ActionHero](https://www.actionherojs.com), built on [Bun](https://bun.sh). I still believe in the core ideas behind ActionHero — it was an attempt to take the best ideas from Rails and Node.js and shove them together — but the original framework needed a fresh start with Bun, Zod, Drizzle, and first-class MCP support.
14
+
15
+ The big idea: **write your controller once, and it works everywhere**. A single action class handles HTTP requests, WebSocket messages, CLI commands, background tasks, and MCP tool calls — same inputs, same validation, same middleware, same response. No duplication.
16
+
17
+ That includes AI agents. Every action is automatically an MCP tool — agents authenticate via built-in OAuth 2.1, get typed errors, and call the same validated endpoints your HTTP clients use. No separate MCP server, no duplicated schemas.
18
+
19
+ ### One Action, Every Transport
20
+
21
+ Here's what that looks like in practice. This is one action:
22
+
23
+ ```ts
24
+ export class UserCreate implements Action {
25
+ name = "user:create";
26
+ description = "Create a new user";
27
+ inputs = z.object({
28
+ name: z.string().min(3),
29
+ email: z.string().email(),
30
+ password: secret(z.string().min(8)),
31
+ });
32
+ web = { route: "/user", method: HTTP_METHOD.PUT };
33
+ task = { queue: "default" };
34
+
35
+ async run(params: ActionParams<UserCreate>) {
36
+ const user = await createUser(params);
37
+ return { user: serializeUser(user) };
38
+ }
39
+ }
40
+ ```
41
+
42
+ That one class gives you:
43
+
44
+ **HTTP** — `PUT /api/user` with JSON body, query params, or form data:
45
+ ```bash
46
+ curl -X PUT http://localhost:8080/api/user \
47
+ -H "Content-Type: application/json" \
48
+ -d '{"name":"Evan","email":"evan@example.com","password":"secret123"}'
49
+ ```
50
+
51
+ **WebSocket** — send a JSON message over an open connection:
52
+ ```json
53
+ { "messageType": "action", "action": "user:create",
54
+ "params": { "name": "Evan", "email": "evan@example.com", "password": "secret123" } }
55
+ ```
56
+
57
+ **CLI** — flags are generated from the Zod schema automatically:
58
+ ```bash
59
+ ./keryx.ts "user:create" --name Evan --email evan@example.com --password secret123 -q | jq
60
+ ```
61
+
62
+ **Background Task** — enqueued to a Resque worker via Redis:
63
+ ```ts
64
+ await api.actions.enqueue("user:create", { name: "Evan", email: "evan@example.com", password: "secret123" });
65
+ ```
66
+
67
+ **MCP** — exposed as a tool for AI agents automatically:
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "my-app": {
72
+ "url": "http://localhost:8080/mcp"
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ Same validation, same middleware chain, same `run()` method, same response shape. The only thing that changes is how the request arrives and how the response is delivered.
79
+
80
+ That's it. The agent can now discover all your actions as tools, authenticate via OAuth, and call them with full type validation.
81
+
82
+ ### Key Components
83
+
84
+ - **MCP-native** — every action is an MCP tool with OAuth 2.1 auth, typed errors, and per-session isolation
85
+ - **Transport-agnostic Actions** — HTTP, WebSocket, CLI, background tasks, and MCP from one class
86
+ - **Zod input validation** — type-safe params with automatic error responses and OpenAPI generation
87
+ - **Built-in background tasks** via [node-resque](https://github.com/actionhero/node-resque), with a [fan-out pattern](#fan-out-tasks) for parallel job processing
88
+ - **Strongly-typed frontend integration** — `ActionResponse<MyAction>` gives the frontend type-safe API responses, no code generation needed
89
+ - **Drizzle ORM** with auto-migrations (replacing the old `ah-sequelize-plugin`)
90
+ - **Companion Vite + React frontend** as a separate application (replacing `ah-next-plugin`)
91
+
92
+ ### Why Bun?
93
+
94
+ TypeScript is still the best language for web APIs. But Node.js has stalled — Bun is moving faster and includes everything we need out of the box:
95
+
96
+ - Native TypeScript — no compilation step
97
+ - Built-in test runner
98
+ - Module resolution that just works
99
+ - Fast startup and an excellent packager
100
+ - `fetch` included natively — great for testing
101
+
102
+ ## Project Structure
103
+
104
+ - **root** — a slim `package.json` wrapping the workspaces. `bun install` and `bun dev` work here, but you need to `cd` into each workspace for tests.
105
+ - **packages/keryx** — the framework package (publishable)
106
+ - **example/backend** — the example backend application
107
+ - **example/frontend** — the example Vite + React frontend
108
+ - **docs** — the [documentation site](https://keryxjs.com)
109
+
110
+ ## Quick Start
111
+
112
+ Create a new project:
113
+
114
+ ```bash
115
+ bunx keryx new my-app
116
+ cd my-app
117
+ cp .env.example .env
118
+ bun install
119
+ bun dev
120
+ ```
121
+
122
+ Requires Bun, PostgreSQL, and Redis. See the [Getting Started guide](https://keryxjs.com/guide/) for full setup instructions.
123
+
124
+ ### Developing the framework itself
125
+
126
+ If you're contributing to Keryx, clone the monorepo instead:
127
+
128
+ ```bash
129
+ git clone https://github.com/evantahler/keryx.git
130
+ cd keryx
131
+ bun install
132
+ cp example/backend/.env.example example/backend/.env
133
+ cp example/frontend/.env.example example/frontend/.env
134
+ bun dev
135
+ ```
136
+
137
+ ## Production Builds
138
+
139
+ ```bash
140
+ bun compile
141
+ # set NODE_ENV=production in .env
142
+ bun start
143
+ ```
144
+
145
+ ## Databases and Migrations
146
+
147
+ We use [Drizzle](https://orm.drizzle.team) as the ORM. Migrations are derived from schemas — edit your schema files in `schema/*.ts`, then generate and apply:
148
+
149
+ ```bash
150
+ cd example/backend && bun run migrations
151
+ # restart the server — pending migrations auto-apply
152
+ ```
153
+
154
+ ## Actions, CLI Commands, and Tasks
155
+
156
+ Unlike the original ActionHero, we've removed the distinction between actions, CLI commands, and tasks. They're all the same thing now. You can run any action from the CLI, schedule any action as a background task, call any action via HTTP or WebSocket, and expose any action as an MCP tool for AI agents. Same input validation, same responses, same middleware.
157
+
158
+ ### Web Actions
159
+
160
+ Add a `web` property to expose an action as an HTTP endpoint. Routes support `:param` path parameters and RegExp patterns — the route lives on the action itself, no separate `routes.ts` file:
161
+
162
+ ```ts
163
+ web = { route: "/user/:id", method: HTTP_METHOD.GET };
164
+ ```
165
+
166
+ ### WebSocket Actions
167
+
168
+ Enabled by default. Clients send JSON messages with `{ messageType: "action", action: "user:create", params: { ... } }`. The server validates params through the same Zod schema and sends the response back over the socket. WebSocket connections also support channel subscriptions for real-time PubSub.
169
+
170
+ ### CLI Actions
171
+
172
+ Enabled by default. Every action is registered as a CLI command via [Commander](https://github.com/tj/commander.js). The Zod schema generates `--flags` and `--help` text automatically:
173
+
174
+ ```bash
175
+ ./keryx.ts "user:create" --name evan --email "evantahler@gmail.com" --password password -q | jq
176
+ ```
177
+
178
+ The `-q` flag suppresses logs so you get clean JSON. Use `--help` on any action to see its params.
179
+
180
+ ### Task Actions
181
+
182
+ Add a `task` property to schedule an action as a background job. A `queue` is required, and `frequency` is optional for recurring execution:
183
+
184
+ ```ts
185
+ task = { queue: "default", frequency: 1000 * 60 * 60 }; // every hour
186
+ ```
187
+
188
+ ### MCP Actions
189
+
190
+ When the MCP server is enabled (`MCP_SERVER_ENABLED=true`), every action is automatically registered as an [MCP](https://modelcontextprotocol.io) tool. AI agents and LLM clients (Claude Desktop, VS Code, etc.) can discover and call your actions through the standard Model Context Protocol.
191
+
192
+ Action names are converted to valid MCP tool names by replacing `:` with `-` (e.g., `user:create` becomes `user-create`). The action's Zod schema is converted to JSON Schema for tool parameter definitions.
193
+
194
+ To exclude an action from MCP:
195
+
196
+ ```ts
197
+ mcp = { enabled: false };
198
+ ```
199
+
200
+ OAuth 2.1 with PKCE is used for authentication — MCP clients go through a browser-based login flow, and subsequent tool calls carry a Bearer token tied to the authenticated user's session.
201
+
202
+ ### Fan-Out Tasks
203
+
204
+ A parent task can distribute work across many child jobs using `api.actions.fanOut()` for parallel processing. Results are collected automatically in Redis. See the [Tasks guide](https://keryxjs.com/guide/tasks) for full API and examples.
205
+
206
+ ## Coming from ActionHero?
207
+
208
+ Keryx keeps the core ideas but rewrites everything on Bun with first-class MCP support. The biggest changes: unified controllers (actions = tasks = CLI commands = MCP tools), separate frontend/backend applications, Drizzle ORM, and MCP as a first-class transport.
209
+
210
+ See the full [migration guide](https://keryxjs.com/guide/from-actionhero) for details.
211
+
212
+ ## Production Deployment
213
+
214
+ Each application has its own `Dockerfile`, and a `docker-compose.yml` runs them together. You probably won't use this exact setup in production, but it shows how the pieces fit together.
215
+
216
+ ## Documentation
217
+
218
+ Full docs at [keryxjs.com](https://keryxjs.com), including:
219
+ - [Getting Started](https://keryxjs.com/guide/)
220
+ - [Actions Guide](https://keryxjs.com/guide/actions)
221
+ - [API Reference](https://keryxjs.com/reference/actions)
222
+
223
+ <p align="center">
224
+ <img src="https://raw.githubusercontent.com/evantahler/keryx/main/docs/public/images/lion-standing.svg" alt="Keryx lion" width="120" />
225
+ </p>
@@ -13,9 +13,15 @@ import { ErrorType, TypedError } from "./TypedError";
13
13
  /**
14
14
  * Represents a client connection to the server — HTTP request, WebSocket, or internal caller.
15
15
  * Each connection tracks its own session, channel subscriptions, and rate-limit state.
16
- * The generic `T` allows typing the session data shape for your application.
16
+ *
17
+ * @typeParam T - Shape of the session data stored in Redis (persists across requests).
18
+ * @typeParam TMeta - Shape of request-scoped metadata that middleware and actions can
19
+ * read/write during a single `act()` call. Reset to `{}` at the start of each invocation.
17
20
  */
18
- export class Connection<T extends Record<string, any> = Record<string, any>> {
21
+ export class Connection<
22
+ T extends Record<string, any> = Record<string, any>,
23
+ TMeta extends Record<string, any> = Record<string, any>,
24
+ > {
19
25
  /** Transport type identifier (e.g., `"web"`, `"websocket"`). */
20
26
  type: string;
21
27
  /** A human-readable identifier for the connection, typically the remote IP or a session key. */
@@ -36,6 +42,8 @@ export class Connection<T extends Record<string, any> = Record<string, any>> {
36
42
  rateLimitInfo?: RateLimitInfo;
37
43
  /** Request correlation ID for distributed tracing. Propagated from the incoming `X-Request-Id` header when `config.server.web.correlationId.trustProxy` is enabled. */
38
44
  correlationId?: string;
45
+ /** App-defined request-scoped metadata. Reset to `{}` at the start of each `act()` call so that long-lived connections (e.g., WebSockets) don't leak state between actions. */
46
+ metadata: Partial<TMeta>;
39
47
 
40
48
  /**
41
49
  * Create a new connection and register it in `api.connections`.
@@ -60,6 +68,7 @@ export class Connection<T extends Record<string, any> = Record<string, any>> {
60
68
  this.sessionLoaded = false;
61
69
  this.subscriptions = new Set();
62
70
  this.rawConnection = rawConnection;
71
+ this.metadata = {};
63
72
 
64
73
  api.connections.connections.set(this.id, this);
65
74
  }
@@ -84,6 +93,7 @@ export class Connection<T extends Record<string, any> = Record<string, any>> {
84
93
  method: Request["method"] = "",
85
94
  url: string = "",
86
95
  ): Promise<{ response: Object; error?: TypedError }> {
96
+ this.metadata = {};
87
97
  const reqStartTime = new Date().getTime();
88
98
  let loggerResponsePrefix: "OK" | "ERROR" = "OK";
89
99
  let response: Object = {};
package/index.ts CHANGED
@@ -22,6 +22,7 @@ export { HTTP_METHOD } from "./classes/Action";
22
22
  export type { ActionMiddleware } from "./classes/Action";
23
23
  export { CHANNEL_NAME_PATTERN } from "./classes/Channel";
24
24
  export type { ChannelMiddleware } from "./classes/Channel";
25
+ export { Connection } from "./classes/Connection";
25
26
  export { LogLevel } from "./classes/Logger";
26
27
  export { ErrorStatusCodes, ErrorType, TypedError } from "./classes/TypedError";
27
28
  export type { KeryxConfig } from "./config";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keryx",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -73,7 +73,8 @@
73
73
  "test": "tsc && bun test",
74
74
  "compile": "bun build keryx.ts --compile --outfile keryx",
75
75
  "lint": "tsc && prettier --check .",
76
- "format": "tsc && prettier --write ."
76
+ "format": "tsc && prettier --write .",
77
+ "prepublishOnly": "cp ../../README.md ."
77
78
  },
78
79
  "dependencies": {
79
80
  "@modelcontextprotocol/sdk": "^1.26.0",