Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ bun run format

Prettier formats the following file types: `*.ts`, `*.js`, `*.json`, `*.md`

### MCP responses

Model-facing JSON (tool results and resources) is compact: use `jsonResponse` or `JSON.stringify(value)` without indentation.

## Project Structure

- `src/` - Source code directory
Expand Down
4 changes: 2 additions & 2 deletions src/lib/mcp/resource-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function registerJsonResourceCollection(
mimeType: "application/json",
text:
resources.length > 0
? JSON.stringify(resources, null, 2)
? JSON.stringify(resources)
: options.emptyText,
},
],
Expand Down Expand Up @@ -127,7 +127,7 @@ export function registerJsonResourceTemplate(
{
uri: uri.toString(),
mimeType: "application/json",
text: JSON.stringify(resource, null, 2),
text: JSON.stringify(resource),
},
],
};
Expand Down
14 changes: 13 additions & 1 deletion src/lib/mcp/responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import {
} from "@onkernel/sdk";
import { describe, expect, test } from "bun:test";

import { errorResponse, throwToolError } from "@/lib/mcp/responses";
import {
errorResponse,
jsonResponse,
throwToolError,
} from "@/lib/mcp/responses";
import { z } from "zod";

function apiError(status: number, message: string) {
Expand All @@ -30,6 +34,14 @@ function caught(error: unknown) {
throw new Error("throwToolError did not throw");
}

describe("jsonResponse", () => {
test("emits compact JSON", () => {
expect(jsonResponse({ a: 1, b: [2] }).content[0].text).toBe(
'{"a":1,"b":[2]}',
);
});
});

describe("throwToolError classification", () => {
test("names Kernel API failures after their status", () => {
expect(caught(apiError(404, "not found")).name).toBe("KernelApiError404");
Expand Down
2 changes: 1 addition & 1 deletion src/lib/mcp/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function textResponse(text: string) {
}

export function jsonResponse(value: unknown) {
return textResponse(JSON.stringify(value, null, 2) ?? String(value));
return textResponse(JSON.stringify(value) ?? String(value));
}

export function itemsJsonResponse<T, U = T>(
Expand Down
2 changes: 1 addition & 1 deletion src/lib/mcp/tools/auth-connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function safeJsonResponse(value: unknown) {
content: [
{
type: "text" as const,
text: JSON.stringify(value, null, 2),
text: JSON.stringify(value),
},
],
structuredContent: value as Record<string, unknown>,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/mcp/tools/browser-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ function replToolContent(result: BrowserReplResult): ReplToolContent[] {
};

const content: ReplToolContent[] = [
{ type: "text", text: JSON.stringify(summary, null, 2) },
{ type: "text", text: JSON.stringify(summary) },
];
for (const item of result.content ?? []) {
if (item.type === "image") {
Expand Down
2 changes: 0 additions & 2 deletions src/lib/mcp/tools/browsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,6 @@ async function readBrowserTelemetry(
if (rawError) return errorResponse(`error: ${rawError}`);
}

// Single-line JSON rather than the pretty-printed house helpers: a page
// carries up to 100 events and indentation would inflate the token cost.
const serializedResponse = JSON.stringify(response);
if (
params.compact === false &&
Expand Down
4 changes: 2 additions & 2 deletions src/lib/mcp/tools/durable-contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ describe("durable profile contracts", () => {
expect(result.content).toEqual([
{
type: "text",
text: JSON.stringify({ id: "profile-1", name: "Renamed" }, null, 2),
text: JSON.stringify({ id: "profile-1", name: "Renamed" }),
},
]);
}
Expand Down Expand Up @@ -426,7 +426,7 @@ describe("durable proxy contracts", () => {
expect(result.content).toEqual([
{
type: "text",
text: JSON.stringify({ id: "proxy-1", name: "Renamed" }, null, 2),
text: JSON.stringify({ id: "proxy-1", name: "Renamed" }),
},
]);
} finally {
Expand Down
25 changes: 7 additions & 18 deletions src/lib/mcp/tools/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
type McpDependencies,
} from "@/lib/mcp/dependencies";
import { longOperationOptions } from "@/lib/mcp/request-options";
import { throwToolError } from "@/lib/mcp/responses";
import { jsonResponse, throwToolError } from "@/lib/mcp/responses";
import {
projectForOperation,
projectSelectionInputSchema,
Expand Down Expand Up @@ -102,23 +102,12 @@ export function registerShellTool(
? Buffer.from(result.stderr_b64, "base64").toString("utf-8")
: "";

return {
content: [
{
type: "text",
text: JSON.stringify(
{
exit_code: result.exit_code,
duration_ms: result.duration_ms,
stdout,
stderr,
},
null,
2,
),
},
],
};
return jsonResponse({
exit_code: result.exit_code,
duration_ms: result.duration_ms,
stdout,
stderr,
});
} catch (error) {
throwToolError("exec_command", "exec", error);
}
Expand Down
Loading