keryx 0.13.0 → 0.14.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 +14 -4
- package/classes/Action.ts +25 -3
- package/config/server/mcp.ts +5 -0
- package/initializers/actionts.ts +1 -1
- package/initializers/mcp.ts +163 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -187,14 +187,24 @@ task = { queue: "default", frequency: 1000 * 60 * 60 }; // every hour
|
|
|
187
187
|
|
|
188
188
|
### MCP Actions
|
|
189
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.
|
|
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. Actions can also be exposed as MCP **resources** (URI-addressed data) and **prompts** (named templates) via `mcp.resource` and `mcp.prompt`.
|
|
191
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.
|
|
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 and prompt parameter definitions.
|
|
193
193
|
|
|
194
|
-
To exclude an action from MCP:
|
|
194
|
+
To exclude an action from MCP tools:
|
|
195
195
|
|
|
196
196
|
```ts
|
|
197
|
-
mcp = {
|
|
197
|
+
mcp = { tool: false };
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
To expose an action as an MCP resource or prompt:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
// Resource — clients fetch this by URI
|
|
204
|
+
mcp = { tool: false, resource: { uri: "myapp://status", mimeType: "application/json" } };
|
|
205
|
+
|
|
206
|
+
// Prompt — clients invoke this as a named template
|
|
207
|
+
mcp = { tool: false, prompt: { title: "Greeting" } };
|
|
198
208
|
```
|
|
199
209
|
|
|
200
210
|
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.
|
package/classes/Action.ts
CHANGED
|
@@ -19,11 +19,33 @@ export type OAuthActionResponse = {
|
|
|
19
19
|
|
|
20
20
|
export type McpActionConfig = {
|
|
21
21
|
/** Expose this action as an MCP tool (default true) */
|
|
22
|
-
|
|
22
|
+
tool?: boolean;
|
|
23
23
|
/** Tag as the OAuth login action */
|
|
24
24
|
isLoginAction?: boolean;
|
|
25
25
|
/** Tag as the OAuth signup action */
|
|
26
26
|
isSignupAction?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Register this action as an MCP resource.
|
|
29
|
+
* The action's `run()` must return `{ text: string; mimeType?: string }` or `{ blob: string; mimeType?: string }` (base64).
|
|
30
|
+
* URI template variables (e.g., `{userId}` in `keryx://users/{userId}`) are passed as action params.
|
|
31
|
+
*/
|
|
32
|
+
resource?: {
|
|
33
|
+
/** Static URI, e.g. `"keryx://status"`. Mutually exclusive with `uriTemplate`. */
|
|
34
|
+
uri?: string;
|
|
35
|
+
/** URI template (RFC 6570), e.g. `"keryx://users/{userId}"`. Variables become action params. */
|
|
36
|
+
uriTemplate?: string;
|
|
37
|
+
/** MIME type of the resource content (e.g., `"application/json"`, `"text/plain"`) */
|
|
38
|
+
mimeType?: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Register this action as an MCP prompt.
|
|
42
|
+
* The action's `inputs` schema becomes the prompt's argument schema.
|
|
43
|
+
* The action's `run()` must return `{ description?: string; messages: PromptMessage[] }`.
|
|
44
|
+
*/
|
|
45
|
+
prompt?: {
|
|
46
|
+
/** Human-readable display title for the prompt */
|
|
47
|
+
title?: string;
|
|
48
|
+
};
|
|
27
49
|
};
|
|
28
50
|
|
|
29
51
|
export type ActionConstructorInputs = {
|
|
@@ -39,7 +61,7 @@ export type ActionConstructorInputs = {
|
|
|
39
61
|
/** Middleware hooks to run before/after `run()` */
|
|
40
62
|
middleware?: ActionMiddleware[];
|
|
41
63
|
|
|
42
|
-
/** Expose this action via the MCP server (defaults to `{
|
|
64
|
+
/** Expose this action via the MCP server (defaults to `{ tool: true }`) */
|
|
43
65
|
mcp?: McpActionConfig;
|
|
44
66
|
|
|
45
67
|
/** Expose this action via HTTP (defaults: route `/${name}`, method `GET`) */
|
|
@@ -115,7 +137,7 @@ export abstract class Action {
|
|
|
115
137
|
this.inputs = args.inputs;
|
|
116
138
|
this.middleware = args.middleware ?? [];
|
|
117
139
|
this.timeout = args.timeout;
|
|
118
|
-
this.mcp = {
|
|
140
|
+
this.mcp = { tool: true, ...args.mcp };
|
|
119
141
|
this.web = {
|
|
120
142
|
route: args.web?.route ?? `/${this.name}`,
|
|
121
143
|
method: args.web?.method ?? HTTP_METHOD.GET,
|
package/config/server/mcp.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import pkg from "../../package.json";
|
|
1
2
|
import { loadFromEnvIfSet } from "../../util/config";
|
|
2
3
|
|
|
3
4
|
export const configServerMcp = {
|
|
4
5
|
enabled: await loadFromEnvIfSet("MCP_SERVER_ENABLED", false),
|
|
5
6
|
route: await loadFromEnvIfSet("MCP_SERVER_ROUTE", "/mcp"),
|
|
7
|
+
instructions: await loadFromEnvIfSet(
|
|
8
|
+
"MCP_SERVER_INSTRUCTIONS",
|
|
9
|
+
pkg.description as string,
|
|
10
|
+
),
|
|
6
11
|
oauthClientTtl: await loadFromEnvIfSet(
|
|
7
12
|
"MCP_OAUTH_CLIENT_TTL",
|
|
8
13
|
60 * 60 * 24 * 30,
|
package/initializers/actionts.ts
CHANGED
|
@@ -638,7 +638,7 @@ export class Actions extends Initializer {
|
|
|
638
638
|
|
|
639
639
|
for (const a of actions) {
|
|
640
640
|
if (!a.description) a.description = `An Action: ${a.name}`;
|
|
641
|
-
a.mcp = {
|
|
641
|
+
a.mcp = { tool: true, ...a.mcp };
|
|
642
642
|
}
|
|
643
643
|
|
|
644
644
|
logger.info(`loaded ${Object.keys(actions).length} actions`);
|
package/initializers/mcp.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
McpServer,
|
|
3
|
+
ResourceTemplate,
|
|
4
|
+
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
5
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
3
6
|
import colors from "colors";
|
|
4
7
|
import { randomUUID } from "crypto";
|
|
@@ -347,18 +350,18 @@ export class McpInitializer extends Initializer {
|
|
|
347
350
|
}
|
|
348
351
|
|
|
349
352
|
/**
|
|
350
|
-
* Create a new McpServer instance with all actions registered as tools.
|
|
353
|
+
* Create a new McpServer instance with all actions registered as tools, resources, and prompts.
|
|
351
354
|
* Each MCP session gets its own McpServer (the SDK requires 1:1 mapping).
|
|
352
|
-
* Actions with `mcp === false` are excluded from tool registration.
|
|
355
|
+
* Actions with `mcp.tool === false` are excluded from tool registration.
|
|
353
356
|
*/
|
|
354
357
|
function createMcpServer(): McpServer {
|
|
355
358
|
const mcpServer = new McpServer(
|
|
356
359
|
{ name: pkg.name, version: pkg.version },
|
|
357
|
-
{ instructions:
|
|
360
|
+
{ instructions: config.server.mcp.instructions },
|
|
358
361
|
);
|
|
359
362
|
|
|
360
363
|
for (const action of api.actions.actions) {
|
|
361
|
-
if (
|
|
364
|
+
if (action.mcp?.tool === false) continue;
|
|
362
365
|
|
|
363
366
|
const toolName = formatToolName(action.name);
|
|
364
367
|
const toolConfig: {
|
|
@@ -382,7 +385,13 @@ function createMcpServer(): McpServer {
|
|
|
382
385
|
|
|
383
386
|
const clientIp = (authInfo?.extra?.ip as string) || "unknown";
|
|
384
387
|
const mcpSessionId = extra.sessionId || "";
|
|
385
|
-
const connection = new Connection(
|
|
388
|
+
const connection = new Connection(
|
|
389
|
+
"mcp",
|
|
390
|
+
clientIp,
|
|
391
|
+
randomUUID(),
|
|
392
|
+
undefined,
|
|
393
|
+
authInfo?.token,
|
|
394
|
+
);
|
|
386
395
|
|
|
387
396
|
try {
|
|
388
397
|
// If Bearer token was verified, set up authenticated session
|
|
@@ -430,6 +439,154 @@ function createMcpServer(): McpServer {
|
|
|
430
439
|
);
|
|
431
440
|
}
|
|
432
441
|
|
|
442
|
+
// Register actions as MCP resources
|
|
443
|
+
for (const action of api.actions.actions) {
|
|
444
|
+
if (!action.mcp?.resource) continue;
|
|
445
|
+
const { uri, uriTemplate, mimeType } = action.mcp.resource;
|
|
446
|
+
|
|
447
|
+
const readCb = async (
|
|
448
|
+
mcpUri: URL,
|
|
449
|
+
variables: Record<string, string | string[]>,
|
|
450
|
+
extra: any,
|
|
451
|
+
) => {
|
|
452
|
+
const authInfo = extra.authInfo;
|
|
453
|
+
const clientIp = (authInfo?.extra?.ip as string) || "unknown";
|
|
454
|
+
const mcpSessionId = extra.sessionId || "";
|
|
455
|
+
const connection = new Connection(
|
|
456
|
+
"mcp",
|
|
457
|
+
clientIp,
|
|
458
|
+
randomUUID(),
|
|
459
|
+
undefined,
|
|
460
|
+
authInfo?.token,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
if (authInfo?.extra?.userId) {
|
|
465
|
+
await connection.loadSession();
|
|
466
|
+
await connection.updateSession({ userId: authInfo.extra.userId });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const params: Record<string, unknown> = { ...variables };
|
|
470
|
+
const { response, error } = await connection.act(
|
|
471
|
+
action.name,
|
|
472
|
+
params,
|
|
473
|
+
"",
|
|
474
|
+
mcpSessionId,
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
if (error) {
|
|
478
|
+
throw new TypedError({
|
|
479
|
+
message: error.message,
|
|
480
|
+
type: error.type ?? ErrorType.CONNECTION_ACTION_RUN,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const content = response as {
|
|
485
|
+
text?: string;
|
|
486
|
+
blob?: string;
|
|
487
|
+
mimeType?: string;
|
|
488
|
+
};
|
|
489
|
+
const resolvedMimeType =
|
|
490
|
+
content.mimeType ?? mimeType ?? "application/json";
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
contents: [
|
|
494
|
+
{
|
|
495
|
+
uri: mcpUri.toString(),
|
|
496
|
+
mimeType: resolvedMimeType,
|
|
497
|
+
...(content.blob
|
|
498
|
+
? { blob: content.blob }
|
|
499
|
+
: {
|
|
500
|
+
text:
|
|
501
|
+
typeof content.text === "string"
|
|
502
|
+
? content.text
|
|
503
|
+
: JSON.stringify(response),
|
|
504
|
+
}),
|
|
505
|
+
},
|
|
506
|
+
],
|
|
507
|
+
};
|
|
508
|
+
} finally {
|
|
509
|
+
connection.destroy();
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
if (uriTemplate) {
|
|
514
|
+
mcpServer.registerResource(
|
|
515
|
+
formatToolName(action.name),
|
|
516
|
+
new ResourceTemplate(uriTemplate, { list: undefined }),
|
|
517
|
+
{ description: action.description, mimeType },
|
|
518
|
+
readCb,
|
|
519
|
+
);
|
|
520
|
+
} else if (uri) {
|
|
521
|
+
mcpServer.registerResource(
|
|
522
|
+
formatToolName(action.name),
|
|
523
|
+
uri,
|
|
524
|
+
{ description: action.description, mimeType },
|
|
525
|
+
(mcpUri: URL, extra: any) => readCb(mcpUri, {}, extra),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Register actions as MCP prompts
|
|
531
|
+
for (const action of api.actions.actions) {
|
|
532
|
+
if (!action.mcp?.prompt) continue;
|
|
533
|
+
const { title } = action.mcp.prompt;
|
|
534
|
+
|
|
535
|
+
mcpServer.registerPrompt(
|
|
536
|
+
formatToolName(action.name),
|
|
537
|
+
{
|
|
538
|
+
title: title ?? action.name,
|
|
539
|
+
description: action.description,
|
|
540
|
+
// argsSchema expects a Record<string, ZodType> (the shape), not a z.object()
|
|
541
|
+
argsSchema: action.inputs
|
|
542
|
+
? sanitizeSchemaForMcp(action.inputs)?.shape
|
|
543
|
+
: undefined,
|
|
544
|
+
},
|
|
545
|
+
async (args: any, extra: any) => {
|
|
546
|
+
const authInfo = extra.authInfo;
|
|
547
|
+
const clientIp = (authInfo?.extra?.ip as string) || "unknown";
|
|
548
|
+
const mcpSessionId = extra.sessionId || "";
|
|
549
|
+
const connection = new Connection(
|
|
550
|
+
"mcp",
|
|
551
|
+
clientIp,
|
|
552
|
+
randomUUID(),
|
|
553
|
+
undefined,
|
|
554
|
+
authInfo?.token,
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
if (authInfo?.extra?.userId) {
|
|
559
|
+
await connection.loadSession();
|
|
560
|
+
await connection.updateSession({ userId: authInfo.extra.userId });
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const params =
|
|
564
|
+
args && typeof args === "object"
|
|
565
|
+
? (args as Record<string, unknown>)
|
|
566
|
+
: {};
|
|
567
|
+
|
|
568
|
+
const { response, error } = await connection.act(
|
|
569
|
+
action.name,
|
|
570
|
+
params,
|
|
571
|
+
"",
|
|
572
|
+
mcpSessionId,
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
if (error) {
|
|
576
|
+
throw new TypedError({
|
|
577
|
+
message: error.message,
|
|
578
|
+
type: error.type ?? ErrorType.CONNECTION_ACTION_RUN,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return response as any;
|
|
583
|
+
} finally {
|
|
584
|
+
connection.destroy();
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
433
590
|
return mcpServer;
|
|
434
591
|
}
|
|
435
592
|
|