keryx 0.15.4 → 0.16.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/classes/Action.ts +11 -0
- package/config/server/mcp.ts +1 -0
- package/index.ts +2 -1
- package/initializers/mcp.ts +12 -3
- package/package.json +1 -1
- package/util/toMarkdown.ts +170 -0
package/classes/Action.ts
CHANGED
|
@@ -2,6 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
import type { Connection } from "./Connection";
|
|
3
3
|
import type { TypedError } from "./TypedError";
|
|
4
4
|
|
|
5
|
+
export enum MCP_RESPONSE_FORMAT {
|
|
6
|
+
"JSON" = "json",
|
|
7
|
+
"MARKDOWN" = "markdown",
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
export enum HTTP_METHOD {
|
|
6
11
|
"GET" = "GET",
|
|
7
12
|
"POST" = "POST",
|
|
@@ -46,6 +51,12 @@ export type McpActionConfig = {
|
|
|
46
51
|
/** Human-readable display title for the prompt */
|
|
47
52
|
title?: string;
|
|
48
53
|
};
|
|
54
|
+
/**
|
|
55
|
+
* Response format for MCP tool calls.
|
|
56
|
+
* `MCP_RESPONSE_FORMAT.JSON` (default) returns `JSON.stringify(response)`.
|
|
57
|
+
* `MCP_RESPONSE_FORMAT.MARKDOWN` returns a human-readable markdown rendering via `toMarkdown()`.
|
|
58
|
+
*/
|
|
59
|
+
responseFormat?: MCP_RESPONSE_FORMAT;
|
|
49
60
|
};
|
|
50
61
|
|
|
51
62
|
export type ActionConstructorInputs = {
|
package/config/server/mcp.ts
CHANGED
package/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import "./initializers/signals";
|
|
|
18
18
|
import "./initializers/swagger";
|
|
19
19
|
|
|
20
20
|
export * from "./api";
|
|
21
|
-
export { HTTP_METHOD } from "./classes/Action";
|
|
21
|
+
export { HTTP_METHOD, MCP_RESPONSE_FORMAT } 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";
|
|
@@ -32,6 +32,7 @@ export type { WebServer } from "./servers/web";
|
|
|
32
32
|
export { buildProgram } from "./util/cli";
|
|
33
33
|
export { deepMerge, loadFromEnvIfSet } from "./util/config";
|
|
34
34
|
export { globLoader } from "./util/glob";
|
|
35
|
+
export { toMarkdown } from "./util/toMarkdown";
|
|
35
36
|
export {
|
|
36
37
|
isSecret,
|
|
37
38
|
secret,
|
package/initializers/mcp.ts
CHANGED
|
@@ -7,6 +7,7 @@ import colors from "colors";
|
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import * as z4mini from "zod/v4-mini";
|
|
9
9
|
import { api, logger } from "../api";
|
|
10
|
+
import { MCP_RESPONSE_FORMAT } from "../classes/Action";
|
|
10
11
|
import { Connection } from "../classes/Connection";
|
|
11
12
|
import { Initializer } from "../classes/Initializer";
|
|
12
13
|
import { ErrorType, TypedError } from "../classes/TypedError";
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
buildCorsHeaders,
|
|
18
19
|
getExternalOrigin,
|
|
19
20
|
} from "../util/http";
|
|
21
|
+
import { toMarkdown } from "../util/toMarkdown";
|
|
20
22
|
import type { PubSubMessage } from "./pubsub";
|
|
21
23
|
|
|
22
24
|
type McpHandleRequest = (req: Request, ip: string) => Promise<Response>;
|
|
@@ -412,6 +414,7 @@ function createMcpServer(): McpServer {
|
|
|
412
414
|
mcpSessionId,
|
|
413
415
|
);
|
|
414
416
|
|
|
417
|
+
// Errors always use JSON for programmatic handling
|
|
415
418
|
if (error) {
|
|
416
419
|
return {
|
|
417
420
|
content: [
|
|
@@ -427,10 +430,16 @@ function createMcpServer(): McpServer {
|
|
|
427
430
|
};
|
|
428
431
|
}
|
|
429
432
|
|
|
433
|
+
const format = action.mcp?.responseFormat ?? MCP_RESPONSE_FORMAT.JSON;
|
|
434
|
+
const text =
|
|
435
|
+
format === MCP_RESPONSE_FORMAT.MARKDOWN
|
|
436
|
+
? toMarkdown(response, {
|
|
437
|
+
maxDepth: config.server.mcp.markdownDepthLimit,
|
|
438
|
+
})
|
|
439
|
+
: JSON.stringify(response);
|
|
440
|
+
|
|
430
441
|
return {
|
|
431
|
-
content: [
|
|
432
|
-
{ type: "text" as const, text: JSON.stringify(response) },
|
|
433
|
-
],
|
|
442
|
+
content: [{ type: "text" as const, text }],
|
|
434
443
|
};
|
|
435
444
|
} finally {
|
|
436
445
|
connection.destroy();
|
package/package.json
CHANGED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a JavaScript value to a human-readable markdown string.
|
|
3
|
+
* Used by the MCP initializer to format tool responses for LLM consumers.
|
|
4
|
+
*
|
|
5
|
+
* @param obj - The value to convert
|
|
6
|
+
* @param options - Formatting options
|
|
7
|
+
* @param options.maxDepth - Maximum nesting depth before falling back to JSON code block (default 5)
|
|
8
|
+
* @param options.headingLevel - Starting heading level, 2 = ## (default 2)
|
|
9
|
+
* @returns A markdown-formatted string
|
|
10
|
+
*/
|
|
11
|
+
export function toMarkdown(
|
|
12
|
+
obj: unknown,
|
|
13
|
+
options?: { maxDepth?: number; headingLevel?: number },
|
|
14
|
+
): string {
|
|
15
|
+
const maxDepth = options?.maxDepth ?? 5;
|
|
16
|
+
const headingLevel = options?.headingLevel ?? 2;
|
|
17
|
+
return renderValue(obj, maxDepth, headingLevel, 0).trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderValue(
|
|
21
|
+
value: unknown,
|
|
22
|
+
maxDepth: number,
|
|
23
|
+
headingLevel: number,
|
|
24
|
+
currentDepth: number,
|
|
25
|
+
): string {
|
|
26
|
+
if (value === null || value === undefined) return "";
|
|
27
|
+
if (typeof value !== "object") return String(value);
|
|
28
|
+
|
|
29
|
+
if (currentDepth >= maxDepth) {
|
|
30
|
+
return "```json\n" + JSON.stringify(value, null, 2) + "\n```\n";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return renderArray(value, maxDepth, headingLevel, currentDepth);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return renderObject(
|
|
38
|
+
value as Record<string, unknown>,
|
|
39
|
+
maxDepth,
|
|
40
|
+
headingLevel,
|
|
41
|
+
currentDepth,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderObject(
|
|
46
|
+
obj: Record<string, unknown>,
|
|
47
|
+
maxDepth: number,
|
|
48
|
+
headingLevel: number,
|
|
49
|
+
currentDepth: number,
|
|
50
|
+
): string {
|
|
51
|
+
const keys = Object.keys(obj);
|
|
52
|
+
if (keys.length === 0) return "";
|
|
53
|
+
|
|
54
|
+
const isFlat = keys.every((k) => isPrimitive(obj[k]));
|
|
55
|
+
|
|
56
|
+
if (isFlat) {
|
|
57
|
+
return (
|
|
58
|
+
keys.map((k) => `- **${k}**: ${formatPrimitive(obj[k])}`).join("\n") +
|
|
59
|
+
"\n"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Mixed object: use headings for each key, recurse into values
|
|
64
|
+
const parts: string[] = [];
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
const val = obj[key];
|
|
67
|
+
if (isPrimitive(val)) {
|
|
68
|
+
parts.push(`- **${key}**: ${formatPrimitive(val)}`);
|
|
69
|
+
} else {
|
|
70
|
+
const heading = makeHeading(key, headingLevel);
|
|
71
|
+
const body = renderValue(
|
|
72
|
+
val,
|
|
73
|
+
maxDepth,
|
|
74
|
+
headingLevel + 1,
|
|
75
|
+
currentDepth + 1,
|
|
76
|
+
);
|
|
77
|
+
parts.push(`${heading}\n\n${body}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return parts.join("\n") + "\n";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderArray(
|
|
84
|
+
arr: unknown[],
|
|
85
|
+
maxDepth: number,
|
|
86
|
+
headingLevel: number,
|
|
87
|
+
currentDepth: number,
|
|
88
|
+
): string {
|
|
89
|
+
if (arr.length === 0) return "";
|
|
90
|
+
|
|
91
|
+
// Array of primitives → bulleted list
|
|
92
|
+
if (arr.every(isPrimitive)) {
|
|
93
|
+
return arr.map((v) => `- ${formatPrimitive(v)}`).join("\n") + "\n";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Array of uniform objects → table
|
|
97
|
+
if (
|
|
98
|
+
arr.every(isPlainObject) &&
|
|
99
|
+
hasUniformKeys(arr as Record<string, unknown>[])
|
|
100
|
+
) {
|
|
101
|
+
return renderTable(arr as Record<string, unknown>[]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Mixed array → bulleted list with recursive rendering
|
|
105
|
+
return (
|
|
106
|
+
arr
|
|
107
|
+
.map((item) => {
|
|
108
|
+
if (isPrimitive(item)) return `- ${formatPrimitive(item)}`;
|
|
109
|
+
const rendered = renderValue(
|
|
110
|
+
item,
|
|
111
|
+
maxDepth,
|
|
112
|
+
headingLevel,
|
|
113
|
+
currentDepth + 1,
|
|
114
|
+
).trim();
|
|
115
|
+
// Indent multiline content under the bullet
|
|
116
|
+
const lines = rendered.split("\n");
|
|
117
|
+
return `- ${lines[0]}${
|
|
118
|
+
lines.length > 1
|
|
119
|
+
? "\n" +
|
|
120
|
+
lines
|
|
121
|
+
.slice(1)
|
|
122
|
+
.map((l) => ` ${l}`)
|
|
123
|
+
.join("\n")
|
|
124
|
+
: ""
|
|
125
|
+
}`;
|
|
126
|
+
})
|
|
127
|
+
.join("\n") + "\n"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderTable(rows: Record<string, unknown>[]): string {
|
|
132
|
+
const keys = Object.keys(rows[0]);
|
|
133
|
+
const header = `| ${keys.join(" | ")} |`;
|
|
134
|
+
const separator = `| ${keys.map(() => "---").join(" | ")} |`;
|
|
135
|
+
const body = rows
|
|
136
|
+
.map(
|
|
137
|
+
(row) =>
|
|
138
|
+
`| ${keys.map((k) => escapeTableCell(formatPrimitive(row[k]))).join(" | ")} |`,
|
|
139
|
+
)
|
|
140
|
+
.join("\n");
|
|
141
|
+
return `${header}\n${separator}\n${body}\n`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function makeHeading(text: string, level: number): string {
|
|
145
|
+
if (level <= 6) return `${"#".repeat(level)} ${text}`;
|
|
146
|
+
return `**${text}**`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isPrimitive(value: unknown): boolean {
|
|
150
|
+
return value === null || value === undefined || typeof value !== "object";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isPlainObject(value: unknown): boolean {
|
|
154
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function hasUniformKeys(arr: Record<string, unknown>[]): boolean {
|
|
158
|
+
if (arr.length === 0) return false;
|
|
159
|
+
const firstKeys = Object.keys(arr[0]).sort().join(",");
|
|
160
|
+
return arr.every((obj) => Object.keys(obj).sort().join(",") === firstKeys);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function formatPrimitive(value: unknown): string {
|
|
164
|
+
if (value === null || value === undefined) return "";
|
|
165
|
+
return String(value);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function escapeTableCell(value: string): string {
|
|
169
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
170
|
+
}
|