@stigmer/mcp-server 3.2.2 → 3.3.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/cli/mcp-server-stigmer.js +9 -9
- package/config.d.ts +12 -0
- package/config.d.ts.map +1 -1
- package/config.js +5 -0
- package/config.js.map +1 -1
- package/domains/records/calls.d.ts +48 -0
- package/domains/records/calls.d.ts.map +1 -0
- package/domains/records/calls.js +118 -0
- package/domains/records/calls.js.map +1 -0
- package/domains/records/errors.d.ts +24 -0
- package/domains/records/errors.d.ts.map +1 -0
- package/domains/records/errors.js +79 -0
- package/domains/records/errors.js.map +1 -0
- package/domains/records/tools.d.ts +7 -0
- package/domains/records/tools.d.ts.map +1 -0
- package/domains/records/tools.js +135 -0
- package/domains/records/tools.js.map +1 -0
- package/gen/agent.d.ts +56 -0
- package/gen/agent.d.ts.map +1 -1
- package/gen/agent.js +24 -1
- package/gen/agent.js.map +1 -1
- package/gen/workflow.d.ts +4 -4
- package/gen/workflow.js +1 -1
- package/gen/workflow.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +4 -4
- package/index.js.map +1 -1
- package/package.json +3 -3
- package/server.d.ts +31 -1
- package/server.d.ts.map +1 -1
- package/server.js +41 -3
- package/server.js.map +1 -1
- package/src/config.ts +19 -0
- package/src/domains/records/calls.ts +209 -0
- package/src/domains/records/errors.ts +98 -0
- package/src/domains/records/records.integration.test.ts +313 -0
- package/src/domains/records/tools.ts +206 -0
- package/src/gen/agent.ts +28 -1
- package/src/gen/workflow.ts +1 -1
- package/src/http.integration.test.ts +1 -0
- package/src/index.ts +12 -5
- package/src/server.ts +59 -5
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// In-process integration test for the record tools (search-pattern:
|
|
2
|
+
// real Connect backend with stubbed record services, real MCP client
|
|
3
|
+
// over an in-memory transport).
|
|
4
|
+
//
|
|
5
|
+
// Verifies the T05 contract surface:
|
|
6
|
+
// - the records-only roster is exactly the five tools (T05 R1), with
|
|
7
|
+
// honest annotations surfaced through tools/list (pinning the SDK's
|
|
8
|
+
// annotations support) and NO org argument (agent audience);
|
|
9
|
+
// - the main roster carries the same five tools WITH the org argument
|
|
10
|
+
// (direct audience);
|
|
11
|
+
// - argument → request mapping (typed filter, "in" → is_in, paging);
|
|
12
|
+
// - the records-own error mapper: domain errors pass the server's
|
|
13
|
+
// message verbatim as {error, code, reason, constraint} JSON with
|
|
14
|
+
// ErrorInfo companions; transport errors delegate to the shared
|
|
15
|
+
// classifier (DD-005 SD-6).
|
|
16
|
+
|
|
17
|
+
import { create } from "@bufbuild/protobuf";
|
|
18
|
+
import { Code, ConnectError, type ConnectRouter } from "@connectrpc/connect";
|
|
19
|
+
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
20
|
+
import {
|
|
21
|
+
createServer as createHttp2Server,
|
|
22
|
+
type Http2Server,
|
|
23
|
+
type ServerHttp2Session,
|
|
24
|
+
} from "node:http2";
|
|
25
|
+
import type { AddressInfo } from "node:net";
|
|
26
|
+
|
|
27
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
28
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
29
|
+
import { DatastoreRecordCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_command_pb";
|
|
30
|
+
import {
|
|
31
|
+
DatastoreDescriptionSchema,
|
|
32
|
+
RecordEnvelopeSchema,
|
|
33
|
+
RecordListSchema,
|
|
34
|
+
type FindRecordsRequest,
|
|
35
|
+
type InsertRecordRequest,
|
|
36
|
+
} from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_io_pb";
|
|
37
|
+
import { DatastoreRecordQueryController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_query_pb";
|
|
38
|
+
import { ErrorInfoSchema } from "@stigmer/protos/google/rpc/error_details_pb";
|
|
39
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
40
|
+
|
|
41
|
+
import { configureLogger } from "../../logger";
|
|
42
|
+
import { createRecordsServer, createServer } from "../../server";
|
|
43
|
+
|
|
44
|
+
configureLogger({ level: "error", format: "text" });
|
|
45
|
+
|
|
46
|
+
const RECORD_TOOLS = [
|
|
47
|
+
"describe_datastore",
|
|
48
|
+
"find_records",
|
|
49
|
+
"insert_record",
|
|
50
|
+
"update_record",
|
|
51
|
+
"delete_record",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
let backend: Http2Server;
|
|
55
|
+
let client: Client;
|
|
56
|
+
const openSessions = new Set<ServerHttp2Session>();
|
|
57
|
+
|
|
58
|
+
/** The next stubbed outcome per RPC; tests set these. */
|
|
59
|
+
let findResponse: () => ReturnType<typeof create<typeof RecordListSchema>>;
|
|
60
|
+
let insertOutcome: () => ReturnType<typeof create<typeof RecordEnvelopeSchema>>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Requests the stubs captured. An object (not two `let`s) because TS
|
|
64
|
+
* does not reset let-narrowing across awaited calls for closure
|
|
65
|
+
* assignments — property narrowing it does.
|
|
66
|
+
*/
|
|
67
|
+
const captured: {
|
|
68
|
+
find?: FindRecordsRequest;
|
|
69
|
+
insert?: InsertRecordRequest;
|
|
70
|
+
} = {};
|
|
71
|
+
|
|
72
|
+
interface ToolResult {
|
|
73
|
+
content: Array<{ type: string; text?: string }>;
|
|
74
|
+
isError?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function callTool(name: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
78
|
+
return (await client.callTool({ name, arguments: args })) as ToolResult;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
beforeAll(async () => {
|
|
82
|
+
const routes = (router: ConnectRouter) => {
|
|
83
|
+
router.service(DatastoreRecordQueryController, {
|
|
84
|
+
findRecords: (req) => {
|
|
85
|
+
captured.find = req;
|
|
86
|
+
return findResponse();
|
|
87
|
+
},
|
|
88
|
+
describeDatastore: () =>
|
|
89
|
+
create(DatastoreDescriptionSchema, {
|
|
90
|
+
datastore: "clinic",
|
|
91
|
+
timezone: "Asia/Kolkata",
|
|
92
|
+
partitions: ["default"],
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
router.service(DatastoreRecordCommandController, {
|
|
96
|
+
insertRecord: (req) => {
|
|
97
|
+
captured.insert = req;
|
|
98
|
+
return insertOutcome();
|
|
99
|
+
},
|
|
100
|
+
updateRecord: () => create(RecordEnvelopeSchema, { id: "dsr_updated" }),
|
|
101
|
+
deleteRecord: () => create(RecordEnvelopeSchema, { id: "dsr_deleted" }),
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
105
|
+
backend.on("session", (session) => {
|
|
106
|
+
openSessions.add(session);
|
|
107
|
+
session.on("close", () => openSessions.delete(session));
|
|
108
|
+
});
|
|
109
|
+
await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
|
|
110
|
+
const port = (backend.address() as AddressInfo).port;
|
|
111
|
+
|
|
112
|
+
// The records-only roster is the surface under test — the audience
|
|
113
|
+
// the runner-synthesized attachment serves.
|
|
114
|
+
const mcp = createRecordsServer({ serverAddress: `127.0.0.1:${port}`, apiKey: "" });
|
|
115
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
116
|
+
client = new Client({ name: "records-integration", version: "test" });
|
|
117
|
+
await Promise.all([mcp.connect(serverTransport), client.connect(clientTransport)]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterAll(async () => {
|
|
121
|
+
await client?.close();
|
|
122
|
+
for (const session of openSessions) session.destroy();
|
|
123
|
+
await new Promise<void>((resolve) => backend.close(() => resolve()));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("records roster (T05 R1)", () => {
|
|
127
|
+
it("exposes exactly the five record tools with honest annotations and no org argument", async () => {
|
|
128
|
+
const { tools } = await client.listTools();
|
|
129
|
+
expect(tools.map((t) => t.name).sort()).toEqual([...RECORD_TOOLS].sort());
|
|
130
|
+
|
|
131
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
132
|
+
// Annotations are part of the DD-005 contract for external clients
|
|
133
|
+
// and the classifier; this pins the SDK actually surfacing them.
|
|
134
|
+
expect(byName.get("find_records")?.annotations).toMatchObject({ readOnlyHint: true });
|
|
135
|
+
expect(byName.get("describe_datastore")?.annotations).toMatchObject({ readOnlyHint: true });
|
|
136
|
+
expect(byName.get("update_record")?.annotations).toMatchObject({ idempotentHint: true });
|
|
137
|
+
expect(byName.get("delete_record")?.annotations).toMatchObject({
|
|
138
|
+
destructiveHint: true,
|
|
139
|
+
idempotentHint: true,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// The agent audience gets no org argument — a session-bound
|
|
143
|
+
// caller's org is server-derived, and offering the argument would
|
|
144
|
+
// only invite INVALID_ARGUMENT rejections (T05 R3).
|
|
145
|
+
for (const tool of tools) {
|
|
146
|
+
const properties = (tool.inputSchema as { properties?: Record<string, unknown> }).properties;
|
|
147
|
+
expect(Object.keys(properties ?? {}), tool.name).not.toContain("org");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("the main roster carries the five tools WITH the org argument", async () => {
|
|
152
|
+
// Direct principals (external MCP clients) must be able to name the
|
|
153
|
+
// org their credential does not carry (DD-006 session-14 amendment).
|
|
154
|
+
const full = createServer({ serverAddress: "127.0.0.1:1", apiKey: "" });
|
|
155
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
156
|
+
const fullClient = new Client({ name: "records-full-roster", version: "test" });
|
|
157
|
+
await Promise.all([full.connect(serverTransport), fullClient.connect(clientTransport)]);
|
|
158
|
+
try {
|
|
159
|
+
const { tools } = await fullClient.listTools();
|
|
160
|
+
const names = tools.map((t) => t.name);
|
|
161
|
+
for (const name of RECORD_TOOLS) {
|
|
162
|
+
expect(names).toContain(name);
|
|
163
|
+
}
|
|
164
|
+
const find = tools.find((t) => t.name === "find_records");
|
|
165
|
+
const properties = (find?.inputSchema as { properties?: Record<string, unknown> })
|
|
166
|
+
.properties;
|
|
167
|
+
expect(Object.keys(properties ?? {})).toContain("org");
|
|
168
|
+
} finally {
|
|
169
|
+
await fullClient.close();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("argument → request mapping", () => {
|
|
175
|
+
it("builds the typed filter (in → is_in), order_by, and paging", async () => {
|
|
176
|
+
findResponse = () =>
|
|
177
|
+
create(RecordListSchema, {
|
|
178
|
+
records: [create(RecordEnvelopeSchema, { id: "dsr_1" })],
|
|
179
|
+
total: 1,
|
|
180
|
+
limit: 25,
|
|
181
|
+
});
|
|
182
|
+
captured.find = undefined;
|
|
183
|
+
|
|
184
|
+
const result = await callTool("find_records", {
|
|
185
|
+
datastore: "clinic",
|
|
186
|
+
collection: "bookings",
|
|
187
|
+
conditions: [
|
|
188
|
+
{ field: "status", op: "eq", value: "confirmed" },
|
|
189
|
+
{ field: "patient_name", op: "in", values: ["Asha", "Ravi"] },
|
|
190
|
+
],
|
|
191
|
+
order_by: { field: "slot_start", direction: "desc" },
|
|
192
|
+
limit: 10,
|
|
193
|
+
offset: 20,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(result.isError).toBeFalsy();
|
|
197
|
+
// Assertion, not annotation: control-flow analysis otherwise pins the
|
|
198
|
+
// const to the `= undefined` reset above (the stub assigns in a
|
|
199
|
+
// closure the analysis cannot see across the await).
|
|
200
|
+
const req = captured.find as FindRecordsRequest | undefined;
|
|
201
|
+
expect(req?.datastore).toBe("clinic");
|
|
202
|
+
expect(req?.org).toBe(""); // agent audience: org never travels
|
|
203
|
+
expect(req?.partition).toBe(""); // partition is never a tool argument (DD-010)
|
|
204
|
+
expect(req?.filter?.conditions).toHaveLength(2);
|
|
205
|
+
expect(req?.filter?.conditions[0]).toMatchObject({ field: "status", op: 1 /* eq */ });
|
|
206
|
+
expect(req?.filter?.conditions[1]).toMatchObject({ field: "patient_name", op: 7 /* is_in */ });
|
|
207
|
+
expect(req?.filter?.conditions[1]?.values).toHaveLength(2);
|
|
208
|
+
expect(req?.orderBy).toMatchObject({ field: "slot_start", direction: 2 /* desc */ });
|
|
209
|
+
expect(req?.limit).toBe(10);
|
|
210
|
+
expect(req?.offset).toBe(20);
|
|
211
|
+
|
|
212
|
+
const body = JSON.parse(result.content[0]?.text ?? "{}") as { total?: number };
|
|
213
|
+
expect(body.total).toBe(1);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("passes the record payload through as a Struct, untouched", async () => {
|
|
217
|
+
insertOutcome = () => create(RecordEnvelopeSchema, { id: "dsr_new" });
|
|
218
|
+
captured.insert = undefined;
|
|
219
|
+
|
|
220
|
+
const result = await callTool("insert_record", {
|
|
221
|
+
datastore: "clinic",
|
|
222
|
+
collection: "bookings",
|
|
223
|
+
record: { slot_start: "2026-07-21T04:30:00Z", patient_name: "Asha" },
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(result.isError).toBeFalsy();
|
|
227
|
+
const req = captured.insert as InsertRecordRequest | undefined;
|
|
228
|
+
expect(req?.record).toMatchObject({
|
|
229
|
+
slot_start: "2026-07-21T04:30:00Z",
|
|
230
|
+
patient_name: "Asha",
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("records error mapper (DD-005 SD-6)", () => {
|
|
236
|
+
it("passes a constraint violation through verbatim with ErrorInfo companions", async () => {
|
|
237
|
+
insertOutcome = () => {
|
|
238
|
+
throw new ConnectError("that slot is already booked", Code.AlreadyExists, undefined, [
|
|
239
|
+
{
|
|
240
|
+
desc: ErrorInfoSchema,
|
|
241
|
+
value: create(ErrorInfoSchema, {
|
|
242
|
+
reason: "CONSTRAINT_VIOLATION",
|
|
243
|
+
domain: "datastore.stigmer.ai",
|
|
244
|
+
metadata: { constraint: "one_confirmed_per_slot" },
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
]);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = await callTool("insert_record", {
|
|
251
|
+
datastore: "clinic",
|
|
252
|
+
collection: "bookings",
|
|
253
|
+
record: { slot_start: "2026-07-21T04:30:00Z", patient_name: "Ravi" },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(result.isError).toBe(true);
|
|
257
|
+
expect(JSON.parse(result.content[0]?.text ?? "{}")).toEqual({
|
|
258
|
+
error: "that slot is already booked",
|
|
259
|
+
code: "ALREADY_EXISTS",
|
|
260
|
+
reason: "CONSTRAINT_VIOLATION",
|
|
261
|
+
constraint: "one_confirmed_per_slot",
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("passes a permission denial through verbatim — never the shared rewrite", async () => {
|
|
266
|
+
insertOutcome = () => {
|
|
267
|
+
throw new ConnectError(
|
|
268
|
+
"you are not allowed to insert records in schedule_exceptions",
|
|
269
|
+
Code.PermissionDenied,
|
|
270
|
+
undefined,
|
|
271
|
+
[
|
|
272
|
+
{
|
|
273
|
+
desc: ErrorInfoSchema,
|
|
274
|
+
value: create(ErrorInfoSchema, {
|
|
275
|
+
reason: "VERB_DENIED",
|
|
276
|
+
domain: "datastore.stigmer.ai",
|
|
277
|
+
}),
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const result = await callTool("insert_record", {
|
|
284
|
+
datastore: "clinic",
|
|
285
|
+
collection: "schedule_exceptions",
|
|
286
|
+
record: { exception_date: "2026-07-22" },
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
expect(result.isError).toBe(true);
|
|
290
|
+
const body = JSON.parse(result.content[0]?.text ?? "{}") as Record<string, string>;
|
|
291
|
+
// The relayable domain bytes — NOT "Check your API key permissions".
|
|
292
|
+
expect(body.error).toBe("you are not allowed to insert records in schedule_exceptions");
|
|
293
|
+
expect(body.code).toBe("PERMISSION_DENIED");
|
|
294
|
+
expect(body.reason).toBe("VERB_DENIED");
|
|
295
|
+
expect(body.constraint).toBeUndefined();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("delegates transport codes to the shared classifier", async () => {
|
|
299
|
+
findResponse = () => {
|
|
300
|
+
throw new ConnectError("upstream down", Code.Unavailable);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const result = await callTool("find_records", {
|
|
304
|
+
datastore: "clinic",
|
|
305
|
+
collection: "bookings",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(result.isError).toBe(true);
|
|
309
|
+
expect(result.content[0]?.text).toBe(
|
|
310
|
+
"Stigmer server is unavailable. Ensure it is running and reachable.",
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// MCP tools for the Datastore record domain — the five agent-facing
|
|
2
|
+
// record tools of DD-005: 1:1 projections of the record RPCs, with the
|
|
3
|
+
// records-own error mapper (errors.ts) and honest MCP annotations (the
|
|
4
|
+
// bridge's first use of annotations; the platform's own connect-time
|
|
5
|
+
// classifier honors destructiveHint fail-closed for external clients,
|
|
6
|
+
// while the runner-synthesized attachment is approval-free by
|
|
7
|
+
// construction and never passes through that classifier).
|
|
8
|
+
//
|
|
9
|
+
// Two audiences, one definition (registered per roster):
|
|
10
|
+
// - "agent" (the records-only roster the runner-synthesized
|
|
11
|
+
// attachment connects to): NO `org` argument — a session-bound
|
|
12
|
+
// caller's org is server-derived from the session, and an explicit
|
|
13
|
+
// org is rejected (T05 R3), so offering the argument would only
|
|
14
|
+
// invite rejected calls.
|
|
15
|
+
// - "direct" (the main roster external MCP clients see): optional
|
|
16
|
+
// `org`, which cloud direct principals must name (their credential
|
|
17
|
+
// carries no ambient org — the DD-006 session-14 amendment). OSS
|
|
18
|
+
// resolves an empty org to the local system org.
|
|
19
|
+
// `partition` is deliberately NOT a tool argument for either audience
|
|
20
|
+
// (DD-010: never from tool arguments); partition-explicit work is the
|
|
21
|
+
// console/CLI/SDK surface.
|
|
22
|
+
|
|
23
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
|
+
import { z } from "zod";
|
|
25
|
+
|
|
26
|
+
import { resolveToken, type BackendTarget } from "../client.js";
|
|
27
|
+
import {
|
|
28
|
+
deleteRecord,
|
|
29
|
+
describeDatastore,
|
|
30
|
+
findRecords,
|
|
31
|
+
insertRecord,
|
|
32
|
+
updateRecord,
|
|
33
|
+
type ConditionArg,
|
|
34
|
+
} from "./calls.js";
|
|
35
|
+
import { recordResult } from "./errors.js";
|
|
36
|
+
|
|
37
|
+
/** Which roster the tools are being registered on (see file header). */
|
|
38
|
+
export type RecordToolAudience = "agent" | "direct";
|
|
39
|
+
|
|
40
|
+
const ENCODINGS =
|
|
41
|
+
"Field value encodings: date YYYY-MM-DD, time HH:MM[:SS], timestamp RFC 3339 UTC " +
|
|
42
|
+
"(e.g. 2026-07-21T04:30:00Z).";
|
|
43
|
+
|
|
44
|
+
const scalar = z.union([z.string(), z.number(), z.boolean()]);
|
|
45
|
+
|
|
46
|
+
const conditionShape = z.object({
|
|
47
|
+
field: z
|
|
48
|
+
.string()
|
|
49
|
+
.describe("Declared field name, or a filterable system field (id, created_at, updated_at)."),
|
|
50
|
+
op: z
|
|
51
|
+
.enum(["eq", "neq", "gt", "gte", "lt", "lte", "in", "not_in", "is_null", "not_null"])
|
|
52
|
+
.describe(
|
|
53
|
+
"Comparison operator. eq/neq/in/not_in for string and enum fields; " +
|
|
54
|
+
"gt/gte/lt/lte for numeric and temporal fields; is_null/not_null for optional fields.",
|
|
55
|
+
),
|
|
56
|
+
value: scalar
|
|
57
|
+
.nullable()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe("Comparison value for scalar operators, in the field's canonical encoding."),
|
|
60
|
+
values: z.array(scalar).optional().describe("Comparison values for in / not_in."),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/** Register the five record tools for one audience; returns the tool names. */
|
|
64
|
+
export function registerRecordTools(
|
|
65
|
+
server: McpServer,
|
|
66
|
+
target: BackendTarget,
|
|
67
|
+
audience: RecordToolAudience,
|
|
68
|
+
): string[] {
|
|
69
|
+
// The org argument exists only on the direct roster (file header).
|
|
70
|
+
const orgShape: { org?: z.ZodOptional<z.ZodString> } = {};
|
|
71
|
+
if (audience === "direct") {
|
|
72
|
+
orgShape.org = z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe(
|
|
76
|
+
"Organization that owns the datastore. Required on Stigmer Cloud; " +
|
|
77
|
+
"omit against a local backend.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
server.registerTool(
|
|
82
|
+
"describe_datastore",
|
|
83
|
+
{
|
|
84
|
+
description:
|
|
85
|
+
"Describe a datastore: its collections, field declarations and encodings, constraint " +
|
|
86
|
+
"messages, data partitions, and the verbs you are allowed to use per collection " +
|
|
87
|
+
"(empty access means you are not allowed to touch that collection). Call this before " +
|
|
88
|
+
`the first record operation against an unfamiliar datastore. ${ENCODINGS}`,
|
|
89
|
+
inputSchema: {
|
|
90
|
+
datastore: z.string().describe("Datastore slug (e.g. clinic)."),
|
|
91
|
+
...orgShape,
|
|
92
|
+
},
|
|
93
|
+
annotations: { readOnlyHint: true },
|
|
94
|
+
},
|
|
95
|
+
(args, extra) =>
|
|
96
|
+
recordResult(`datastore "${args.datastore}"`, () =>
|
|
97
|
+
describeDatastore(target.serverAddress, resolveToken(extra, target.apiKey), args),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
server.registerTool(
|
|
102
|
+
"find_records",
|
|
103
|
+
{
|
|
104
|
+
description:
|
|
105
|
+
"Find records in a datastore collection with a typed filter (conditions are AND-combined). " +
|
|
106
|
+
"Returns records plus total/limit/offset for paging. Results are ordered by created_at " +
|
|
107
|
+
`descending unless order_by is given. ${ENCODINGS}`,
|
|
108
|
+
inputSchema: {
|
|
109
|
+
datastore: z.string().describe("Datastore slug (e.g. clinic)."),
|
|
110
|
+
...orgShape,
|
|
111
|
+
collection: z.string().describe("Collection to query (e.g. bookings)."),
|
|
112
|
+
conditions: z
|
|
113
|
+
.array(conditionShape)
|
|
114
|
+
.optional()
|
|
115
|
+
.describe("Filter conditions, AND-combined. Omit to list all readable records."),
|
|
116
|
+
order_by: z
|
|
117
|
+
.object({
|
|
118
|
+
field: z.string().describe("Field to sort by."),
|
|
119
|
+
direction: z.enum(["asc", "desc"]).optional().describe("Sort direction (default asc)."),
|
|
120
|
+
})
|
|
121
|
+
.optional()
|
|
122
|
+
.describe("Sort order. Omit for created_at descending with id tiebreak."),
|
|
123
|
+
limit: z.number().int().optional().describe("Page size (default 25, max 100)."),
|
|
124
|
+
offset: z.number().int().optional().describe("Records to skip for paging."),
|
|
125
|
+
},
|
|
126
|
+
annotations: { readOnlyHint: true },
|
|
127
|
+
},
|
|
128
|
+
(args, extra) =>
|
|
129
|
+
recordResult(`records in "${args.datastore}/${args.collection}"`, () =>
|
|
130
|
+
findRecords(target.serverAddress, resolveToken(extra, target.apiKey), {
|
|
131
|
+
...args,
|
|
132
|
+
conditions: args.conditions as ConditionArg[] | undefined,
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
server.registerTool(
|
|
138
|
+
"insert_record",
|
|
139
|
+
{
|
|
140
|
+
description:
|
|
141
|
+
"Insert one record into a datastore collection. System fields (id, created_at, " +
|
|
142
|
+
"updated_at, created_by) are server-stamped — never include them. Declared constraints " +
|
|
143
|
+
"are enforced by the store; a violation returns the constraint's message verbatim " +
|
|
144
|
+
"(e.g. a taken slot returns ALREADY_EXISTS — pick another, do not retry the same one). " +
|
|
145
|
+
`When uncertain whether a record already exists, find first. ${ENCODINGS}`,
|
|
146
|
+
inputSchema: {
|
|
147
|
+
datastore: z.string().describe("Datastore slug (e.g. clinic)."),
|
|
148
|
+
...orgShape,
|
|
149
|
+
collection: z.string().describe("Collection to insert into (e.g. bookings)."),
|
|
150
|
+
record: z
|
|
151
|
+
.record(z.unknown())
|
|
152
|
+
.describe("Declared fields for the new record, in canonical encodings."),
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
(args, extra) =>
|
|
156
|
+
recordResult(`record in "${args.datastore}/${args.collection}"`, () =>
|
|
157
|
+
insertRecord(target.serverAddress, resolveToken(extra, target.apiKey), args),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
server.registerTool(
|
|
162
|
+
"update_record",
|
|
163
|
+
{
|
|
164
|
+
description:
|
|
165
|
+
"Update one record by id with a partial merge: only the supplied fields change, and an " +
|
|
166
|
+
"explicit null clears a field. Constraints are re-evaluated on the merged result. " +
|
|
167
|
+
`Returns the full updated record. ${ENCODINGS}`,
|
|
168
|
+
inputSchema: {
|
|
169
|
+
datastore: z.string().describe("Datastore slug (e.g. clinic)."),
|
|
170
|
+
...orgShape,
|
|
171
|
+
collection: z.string().describe("Collection holding the record."),
|
|
172
|
+
id: z.string().describe("Record id (from a previous find or insert)."),
|
|
173
|
+
fields: z
|
|
174
|
+
.record(z.unknown())
|
|
175
|
+
.describe("Fields to merge. Explicit null clears a field; omitted fields keep their values."),
|
|
176
|
+
},
|
|
177
|
+
annotations: { idempotentHint: true },
|
|
178
|
+
},
|
|
179
|
+
(args, extra) =>
|
|
180
|
+
recordResult(`record "${args.id}" in "${args.datastore}/${args.collection}"`, () =>
|
|
181
|
+
updateRecord(target.serverAddress, resolveToken(extra, target.apiKey), args),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
server.registerTool(
|
|
186
|
+
"delete_record",
|
|
187
|
+
{
|
|
188
|
+
description:
|
|
189
|
+
"Delete one record by id. Returns the deleted record. Record tools never delete " +
|
|
190
|
+
"collections or datastores.",
|
|
191
|
+
inputSchema: {
|
|
192
|
+
datastore: z.string().describe("Datastore slug (e.g. clinic)."),
|
|
193
|
+
...orgShape,
|
|
194
|
+
collection: z.string().describe("Collection holding the record."),
|
|
195
|
+
id: z.string().describe("Record id (from a previous find or insert)."),
|
|
196
|
+
},
|
|
197
|
+
annotations: { destructiveHint: true, idempotentHint: true },
|
|
198
|
+
},
|
|
199
|
+
(args, extra) =>
|
|
200
|
+
recordResult(`record "${args.id}" in "${args.datastore}/${args.collection}"`, () =>
|
|
201
|
+
deleteRecord(target.serverAddress, resolveToken(extra, target.apiKey), args),
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
return ["describe_datastore", "find_records", "insert_record", "update_record", "delete_record"];
|
|
206
|
+
}
|
package/src/gen/agent.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { generateSlug, visibilityFromString } from "./apply-runtime.js";
|
|
7
7
|
import { create } from "@bufbuild/protobuf";
|
|
8
8
|
import { AgentSchema, type Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
9
|
-
import { AgentSpecSchema, ToolApprovalOverrideSchema, McpServerUsageSchema, McpAccessSchema, SubAgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
9
|
+
import { AgentSpecSchema, ToolApprovalOverrideSchema, McpServerUsageSchema, McpAccessSchema, SubAgentSchema, DatastoreUsageSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
10
10
|
import { EnvVarDeclarationSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
|
|
11
11
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
12
12
|
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
@@ -28,6 +28,7 @@ export const AgentInputShape = {
|
|
|
28
28
|
skill_refs: z.array(z.lazy(() => SkillRefInputSchema)).optional().describe("Skill resources providing additional knowledge to the agent."),
|
|
29
29
|
sub_agents: z.array(z.lazy(() => SubAgentInputSchema)).optional().describe("Sub-agents that can be delegated to. Sub-agents can access a subset of the parent's MCP servers and tools."),
|
|
30
30
|
env: z.record(z.lazy(() => EnvVarDeclarationInputSchema)).optional().describe("Environment variable declarations for this agent. Keys are variable names; values describe their metadata and optionality."),
|
|
31
|
+
datastore_usages: z.array(z.lazy(() => DatastoreUsageInputSchema)).optional().describe("Datastores this agent can use. Each entry must reference a Datastore resource by slug."),
|
|
31
32
|
} as const;
|
|
32
33
|
|
|
33
34
|
export const AgentInputSchema = z.object(AgentInputShape);
|
|
@@ -83,6 +84,17 @@ const EnvVarDeclarationInputSchema = z.object({
|
|
|
83
84
|
});
|
|
84
85
|
type EnvVarDeclarationInput = z.infer<typeof EnvVarDeclarationInputSchema>;
|
|
85
86
|
|
|
87
|
+
const DatastoreRefInputSchema = z.object({
|
|
88
|
+
org: z.string().optional().describe("Organization that owns the referenced resource. When non-empty: must be a valid org slug (lowercase alphanumeric with hyphens, starts with a letter, 1-63 characters). Example: 'stigmer', 'acme-corp'. When empty: the reference is relative — the server resolves it to the parent resource's organization at write time. All stored and returned references always have org populated (absolute form). Use empty org for same-org references (the common case). Use explicit org for cross-org references (e.g., marketplace resources)."),
|
|
89
|
+
slug: z.string().describe("Resource slug (user-friendly identifier, unique within org). Format: lowercase alphanumeric with hyphens, must start with a letter and end with a letter or digit (e.g., 'web-search', 'code-reviewer'). Length: 2-63 characters."),
|
|
90
|
+
});
|
|
91
|
+
type DatastoreRefInput = z.infer<typeof DatastoreRefInputSchema>;
|
|
92
|
+
|
|
93
|
+
const DatastoreUsageInputSchema = z.object({
|
|
94
|
+
datastore_ref: z.lazy(() => DatastoreRefInputSchema).describe("Reference to the Datastore resource."),
|
|
95
|
+
});
|
|
96
|
+
type DatastoreUsageInput = z.infer<typeof DatastoreUsageInputSchema>;
|
|
97
|
+
|
|
86
98
|
|
|
87
99
|
/** Build the fully-formed Agent proto from the flat MCP apply input. */
|
|
88
100
|
export function agentInputToProto(input: AgentInput): Agent {
|
|
@@ -97,6 +109,7 @@ export function agentInputToProto(input: AgentInput): Agent {
|
|
|
97
109
|
if (input.env !== undefined) {
|
|
98
110
|
for (const [k, v] of Object.entries(input.env)) spec.env[k] = envVarDeclarationInputToProto(v);
|
|
99
111
|
}
|
|
112
|
+
if (input.datastore_usages !== undefined) spec.datastoreUsages = input.datastore_usages.map(datastoreUsageInputToProto);
|
|
100
113
|
return Object.assign(create(AgentSchema), {
|
|
101
114
|
apiVersion: "agentic.stigmer.ai/v1",
|
|
102
115
|
kind: "Agent",
|
|
@@ -171,3 +184,17 @@ function envVarDeclarationInputToProto(input: EnvVarDeclarationInput) {
|
|
|
171
184
|
return result;
|
|
172
185
|
}
|
|
173
186
|
|
|
187
|
+
function datastoreRefInputToProto(input: DatastoreRefInput) {
|
|
188
|
+
return create(ApiResourceReferenceSchema, {
|
|
189
|
+
org: input.org,
|
|
190
|
+
slug: input.slug,
|
|
191
|
+
kind: ApiResourceKind.datastore,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function datastoreUsageInputToProto(input: DatastoreUsageInput) {
|
|
196
|
+
const result = create(DatastoreUsageSchema);
|
|
197
|
+
if (input.datastore_ref !== undefined) result.datastoreRef = datastoreRefInputToProto(input.datastore_ref);
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
package/src/gen/workflow.ts
CHANGED
|
@@ -392,7 +392,7 @@ const WorkflowTaskInputSchema: z.ZodType<WorkflowTaskInput> = z.lazy(() => z.obj
|
|
|
392
392
|
fork: z.lazy(() => ForkTaskConfigInputSchema).optional().describe("Required when kind='fork'. ForkTaskConfig defines the configuration for fork tasks that execute branches in parallel. @internal YAML Example: - taskName: fork: branches: - branch1: do: - task1: call: http with: method: POST endpoint: uri: https://api.example.com/branch1 - branch2: do: - task2: call: http Reference: zigflow-dsl-pattern-catalog.md - Task Type 5"),
|
|
393
393
|
grpc_call: z.lazy(() => GrpcCallTaskConfigInputSchema).optional().describe("Required when kind='grpc_call'. GrpcCallTaskConfig defines the configuration for grpc_call tasks that make gRPC requests. @internal YAML Example: - taskName: call: grpc with: service: 'com.example.UserService' method: 'GetUser' request: userId: ${ .userId } Reference: zigflow-dsl-pattern-catalog.md - Task Type 9"),
|
|
394
394
|
http_call: z.lazy(() => HttpCallTaskConfigInputSchema).optional().describe("Required when kind='http_call'. HttpCallTaskConfig defines the configuration for http_call tasks that make HTTP requests. @internal YAML Example: - taskName: call: http with: method: POST endpoint: uri: https://api.example.com/data headers: Authorization: 'Bearer ${TOKEN}' body: field1: value Reference: zigflow-dsl-pattern-catalog.md - Task Type 2"),
|
|
395
|
-
human_input: z.lazy(() => HumanInputTaskConfigInputSchema).optional().describe("Required when kind='human_input'. HumanInputTaskConfig defines the configuration for human_input tasks that pause workflow execution to collect typed input or approval from a human reviewer, then resume based on the reviewer's response. @internal Use human_input when a workflow needs human judgment before proceeding: approvals before API calls, sign-off before publishing, confirmation before customer-impacting actions, or structured data collection from a human operator. This is a workflow-level approval gate — distinct from agent-level HITL (tool approval inside an agent session). Workflow-level gates are visible in the execution viewer as explicit tasks with input/output/timing, and they can route to different branches based on the reviewer's decision. Runtime implementation (T13) will use Temporal signals for workflow resumption. The proto definition is intentionally runtime-agnostic. The reviewer's response (selected outcome + form data) becomes the task output, accessible via export: { 'outcome': 'approve', 'form_data': { <validated form response if form_schema is set> }, 'reviewer': '<user who responded>', 'responded_at': '<ISO 8601 timestamp>' } YAML Example (approval gate with custom outcomes): - manager_approval: human_input: prompt: 'Customer-impacting incident classified as ${ $context.triage.severity }. Approve escalation?' form_schema: type: object properties: notes: type: string description: 'Optional notes for the engineering team' priority_override: type: string enum: [P1, P2, P3] outcomes: - name: approve label: 'Approve Escalation' - name: deny label: 'Reject — Not Customer-Impacting' then: re_classify - name: needs_revision label: 'Needs More Info' then: gather_more_context approvers: - 'team:engineering-leads' timeout: 86400 on_timeout: HUMAN_INPUT_TIMEOUT_ESCALATE notification_channels: - 'slack:#incident-approvals' export: as: '${ . }' YAML Example (simple binary approval, no form): - confirm_publish: human_input: prompt: 'Ready to publish ${ $context.document.title } to production?' approvers: - 'role:content-admin' timeout: 3600 on_timeout: HUMAN_INPUT_TIMEOUT_FAIL export: as: '${ . }'"),
|
|
395
|
+
human_input: z.lazy(() => HumanInputTaskConfigInputSchema).optional().describe("Required when kind='human_input'. HumanInputTaskConfig defines the configuration for human_input tasks that pause workflow execution to collect typed input or approval from a human reviewer, then resume based on the reviewer's response. @internal Use human_input when a workflow needs human judgment before proceeding: approvals before API calls, sign-off before publishing, confirmation before customer-impacting actions, or structured data collection from a human operator. This is a workflow-level approval gate — distinct from agent-level HITL (tool approval inside an agent session). Workflow-level gates are visible in the execution viewer as explicit tasks with input/output/timing, and they can route to different branches based on the reviewer's decision. Runtime implementation (T13) will use Temporal signals for workflow resumption. The proto definition is intentionally runtime-agnostic. The reviewer's response (selected outcome + form data) becomes the task output, accessible via export: { 'outcome': 'approve', 'form_data': { <validated form response if form_schema is set> }, 'reviewer': '<canonical identity of the user who responded>', 'reviewer_actor': { 'id': '<canonical identity>', 'display_name': '<human-readable name, empty when unknown>', 'email': '<email address, empty when unknown>', 'avatar': '<avatar URL, empty when unknown>' }, 'responded_at': '<ISO 8601 timestamp>' } 'reviewer' is the stable audit key (use it in switch conditions and audit joins); 'reviewer_actor' is a display snapshot stamped server-side at decision time (use it in notifications and UIs). Both are absent when the gate resolves without attribution (timeout policies, OSS single-user edition). YAML Example (approval gate with custom outcomes): - manager_approval: human_input: prompt: 'Customer-impacting incident classified as ${ $context.triage.severity }. Approve escalation?' form_schema: type: object properties: notes: type: string description: 'Optional notes for the engineering team' priority_override: type: string enum: [P1, P2, P3] outcomes: - name: approve label: 'Approve Escalation' - name: deny label: 'Reject — Not Customer-Impacting' then: re_classify - name: needs_revision label: 'Needs More Info' then: gather_more_context approvers: - 'team:engineering-leads' timeout: 86400 on_timeout: HUMAN_INPUT_TIMEOUT_ESCALATE notification_channels: - 'slack:#incident-approvals' export: as: '${ . }' YAML Example (simple binary approval, no form): - confirm_publish: human_input: prompt: 'Ready to publish ${ $context.document.title } to production?' approvers: - 'role:content-admin' timeout: 3600 on_timeout: HUMAN_INPUT_TIMEOUT_FAIL export: as: '${ . }'"),
|
|
396
396
|
listen: z.lazy(() => ListenTaskConfigInputSchema).optional().describe("Required when kind='listen'. ListenTaskConfig defines the configuration for listen tasks that wait for external signals. @internal Implemented via Temporal signals. YAML Example: - taskName: listen: to: one: with: id: approval_signal type: signal Reference: zigflow-dsl-pattern-catalog.md - Task Type 7"),
|
|
397
397
|
llm_call: z.lazy(() => LlmCallTaskConfigInputSchema).optional().describe("Required when kind='llm_call'. LlmCallTaskConfig defines the configuration for llm_call tasks that make direct LLM API calls without the overhead of a full agent invocation. @internal Use llm_call when the task is focused and deterministic: classification, extraction, scoring, summarization, moderation, or routing. An agent_call carries setup overhead (system prompt, tool resolution, MCP server setup, session management) that is unnecessary when all you need is a single prompt-response cycle with optional structured output. When response_schema is set, the runner requests structured output from the provider and validates the response against the schema. The on_invalid / max_retries / fallback_task fields control what happens when validation fails, using the same OnInvalidOutputPolicy enum as agent_call's output contract. YAML Example (classification with structured output): - classify_severity: call: llm with: model: 'gpt-4o-mini' system_prompt: 'You are a support ticket classifier.' prompt: 'Classify this ticket: ${ $context.ticket.description }' response_schema: type: object required: [severity, category] properties: severity: type: string enum: [low, medium, high, critical] category: type: string on_invalid: ON_INVALID_RETRY max_retries: 2 export: as: '${ . }' YAML Example (simple summarization, no schema): - summarize: call: llm with: model: 'claude-sonnet-4-5' prompt: 'Summarize in 2 sentences: ${ $context.document.text }' temperature: 0.3 max_tokens: 200 export: as: '${ . }'"),
|
|
398
398
|
notification: z.lazy(() => NotificationTaskConfigInputSchema).optional().describe("Required when kind='notification'. NotificationTaskConfig defines the configuration for notification tasks that send messages to humans through channels like Slack, email, Discord, Microsoft Teams, or webhooks. @internal notification is a convenience abstraction for 'send a message to humans' — simpler and more intuitive than emit_event for common operational notification patterns. While emit_event publishes system-level CloudEvents for machine consumption, notification is purpose-built for human-readable messages with channel routing and recipient targeting. Key distinction from related task types: emit_event = system-to-system (CloudEvents, consumed by workflows/services) notification = system-to-human (Slack, email, Discord — read by people) human_input = system-to-human-to-system (waits for a response; notification is fire-and-forget) Notifications are fire-and-forget by default. The task output confirms delivery was attempted but does not wait for acknowledgment: { 'channel': 'slack', 'recipients': ['#incident-response'], 'delivered': true, 'delivered_at': '<ISO 8601 timestamp>' } For notifications that require acknowledgment or a response, use human_input instead — it pauses the workflow until a human responds. The channel field is a string (not an enum) to allow extensibility: new notification channels can be added without proto changes. The runtime (T13) resolves channel identifiers to actual notification providers configured in the workflow instance's environment. YAML Example (Slack notification): - alert_slack: notification: channel: 'slack' recipients: - '#incident-response' - '@oncall-lead' subject: 'P${ $context.triage.severity } Incident: ${ $context.ticket.title }' body: | *Severity*: ${ $context.triage.severity } *Category*: ${ $context.triage.category } *Customer Impact*: ${ $context.triage.customer_impact } *Summary*: ${ $context.agent_analysis.structured.summary } metadata: thread_ts: '${ $context.slack_thread_id }' export: as: '${ . }' YAML Example (email with template): - send_confirmation: notification: channel: 'email' recipients: - '${ $context.customer.email }' subject: 'Order ${ $context.order.number } Confirmed' body: 'Your order has been confirmed. You will receive tracking information shortly.' template: 'order-confirmation' metadata: priority: 'high' reply_to: 'support@acme.com' export: as: '${ . }'"),
|
package/src/index.ts
CHANGED
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
import { loadConfigFromEnv, validateConfig, type Config } from "./config.js";
|
|
8
8
|
import { configureLogger, log } from "./logger.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
isNormalShutdown,
|
|
11
|
+
routedServerFactory,
|
|
12
|
+
serveBoth,
|
|
13
|
+
serveHttp,
|
|
14
|
+
serveStdio,
|
|
15
|
+
stdioServer,
|
|
16
|
+
} from "./server.js";
|
|
10
17
|
import type { BackendTarget } from "./domains/client.js";
|
|
11
18
|
|
|
12
|
-
export type { Config, OAuthConfig, Transport } from "./config.js";
|
|
19
|
+
export type { Config, OAuthConfig, Roster, Transport } from "./config.js";
|
|
13
20
|
export { loadConfigFromEnv, validateConfig } from "./config.js";
|
|
14
|
-
export { createServer, SERVER_VERSION } from "./server.js";
|
|
21
|
+
export { createServer, createRecordsServer, RECORDS_ROUTE, SERVER_VERSION } from "./server.js";
|
|
15
22
|
|
|
16
23
|
/** Returns a Config populated from environment variables (no validation). */
|
|
17
24
|
export function defaultConfig(): Config {
|
|
@@ -44,10 +51,10 @@ export async function run(cfg: Config, signal: AbortSignal): Promise<void> {
|
|
|
44
51
|
try {
|
|
45
52
|
switch (cfg.transport) {
|
|
46
53
|
case "stdio":
|
|
47
|
-
await serveStdio(
|
|
54
|
+
await serveStdio(stdioServer(target, cfg), signal);
|
|
48
55
|
break;
|
|
49
56
|
case "http":
|
|
50
|
-
await serveHttp((
|
|
57
|
+
await serveHttp(routedServerFactory(target), cfg, signal);
|
|
51
58
|
break;
|
|
52
59
|
case "both":
|
|
53
60
|
await serveBoth(target, cfg, signal);
|