apps/glyph Public The code is public; the app answers only to 1 person who can change it and the crew.

Files acd87b6b6146da11e0c9272c92a02a8e707adf4a
123 lines · 3.6 KB · ~1.1k tok Raw History
1
/**
2
 * An MCP server over plain HTTP: `POST /mcp` takes one JSON-RPC 2.0
3
 * message and answers with JSON (streamable HTTP without the stream —
4
 * nothing here needs to push). The tools are the actions, called with the
5
 * same identity the page gets, so an agent speaking MCP through the forge
6
 * is the person whose grant it carries, and a bare one is anonymous.
7
 */
8
9
import type { Database } from "bun:sqlite";
10
import { addNote, deleteNote, listNotes, type Result } from "./actions.ts";
11
import { title } from "./env.ts";
12
import { fields } from "./http.ts";
13
import type { Who } from "./who.ts";
14
15
interface Tool {
16
  readonly name: string;
17
  readonly description: string;
18
  readonly inputSchema: Record<string, unknown>;
19
  readonly run: (
20
    db: Database,
21
    who: Who,
22
    args: Record<string, unknown>,
23
  ) => Result<unknown>;
24
}
25
26
export const tools: readonly Tool[] = [
27
  {
28
    name: "list_notes",
29
    description: "The notes, newest first.",
30
    inputSchema: {
31
      type: "object",
32
      properties: {
33
        limit: {
34
          type: "integer",
35
          minimum: 1,
36
          maximum: 500,
37
          description: "At most this many (100).",
38
        },
39
      },
40
    },
41
    run: (db, who, args) =>
42
      listNotes(db, who, { limit: Number(args.limit ?? 100) }),
43
  },
44
  {
45
    name: "add_note",
46
    description:
47
      "Add a note as the signed-in person. Anonymous callers cannot.",
48
    inputSchema: {
49
      type: "object",
50
      properties: { body: { type: "string", maxLength: 500 } },
51
      required: ["body"],
52
    },
53
    run: (db, who, args) => addNote(db, who, { body: String(args.body ?? "") }),
54
  },
55
  {
56
    name: "delete_note",
57
    description: "Remove a note by id. Only its author or an admin can.",
58
    inputSchema: {
59
      type: "object",
60
      properties: { id: { type: "integer" } },
61
      required: ["id"],
62
    },
63
    run: (db, who, args) => deleteNote(db, who, { id: Number(args.id) }),
64
  },
65
];
66
67
/** One JSON-RPC message in, an HTTP status and a body out (a notification gets 202 and no body). */
68
export function rpc(
69
  db: Database,
70
  who: Who,
71
  message: unknown,
72
): { status: number; body?: unknown } {
73
  const m = fields(message);
74
  const id = m.id ?? null;
75
  const reply = (result: unknown) => ({
76
    status: 200,
77
    body: { jsonrpc: "2.0", id, result },
78
  });
79
  const refuse = (code: number, text: string) => ({
80
    status: 200,
81
    body: { jsonrpc: "2.0", id, error: { code, message: text } },
82
  });
83
  if (m.jsonrpc !== "2.0" || typeof m.method !== "string")
84
    return refuse(-32600, "not a JSON-RPC 2.0 request");
85
  if (m.id === undefined) return { status: 202 };
86
  const params = fields(m.params);
87
  switch (m.method) {
88
    case "initialize":
89
      return reply({
90
        protocolVersion: "2025-06-18",
91
        capabilities: { tools: {} },
92
        serverInfo: { name: title, version: "1" },
93
      });
94
    case "ping":
95
      return reply({});
96
    case "tools/list":
97
      return reply({
98
        tools: tools.map(({ name, description, inputSchema }) => ({
99
          name,
100
          description,
101
          inputSchema,
102
        })),
103
      });
104
    case "tools/call": {
105
      const tool = tools.find((t) => t.name === params.name);
106
      if (tool === undefined)
107
        return refuse(-32602, `no tool named ${String(params.name)}`);
108
      const result = tool.run(db, who, fields(params.arguments));
109
      return reply(
110
        result.ok
111
          ? { content: [{ type: "text", text: JSON.stringify(result.value) }] }
112
          : {
113
              content: [
114
                { type: "text", text: `${result.error}: ${result.message}` },
115
              ],
116
              isError: true,
117
            },
118
      );
119
    }
120
    default:
121
      return refuse(-32601, `no method ${m.method}`);
122
  }
123
}
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts