@stigmer/mcp-server 3.2.3 → 3.4.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 +60 -13
- package/cli/mcp-server-stigmer.js +13 -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/agentexecutions/approve.d.ts +9 -0
- package/domains/agentexecutions/approve.d.ts.map +1 -0
- package/domains/agentexecutions/approve.js +46 -0
- package/domains/agentexecutions/approve.js.map +1 -0
- package/domains/agentexecutions/fetch.d.ts +28 -0
- package/domains/agentexecutions/fetch.d.ts.map +1 -0
- package/domains/agentexecutions/fetch.js +82 -0
- package/domains/agentexecutions/fetch.js.map +1 -0
- package/domains/agentexecutions/run.d.ts +27 -0
- package/domains/agentexecutions/run.d.ts.map +1 -0
- package/domains/agentexecutions/run.js +87 -0
- package/domains/agentexecutions/run.js.map +1 -0
- package/domains/agentexecutions/tools.d.ts +5 -0
- package/domains/agentexecutions/tools.d.ts.map +1 -0
- package/domains/agentexecutions/tools.js +97 -0
- package/domains/agentexecutions/tools.js.map +1 -0
- package/domains/datastores/apply.d.ts +4 -0
- package/domains/datastores/apply.d.ts.map +1 -0
- package/domains/datastores/apply.js +28 -0
- package/domains/datastores/apply.js.map +1 -0
- package/domains/datastores/delete.d.ts +3 -0
- package/domains/datastores/delete.d.ts.map +1 -0
- package/domains/datastores/delete.js +38 -0
- package/domains/datastores/delete.js.map +1 -0
- package/domains/datastores/fetch.d.ts +3 -0
- package/domains/datastores/fetch.d.ts.map +1 -0
- package/domains/datastores/fetch.js +23 -0
- package/domains/datastores/fetch.js.map +1 -0
- package/domains/datastores/resources.d.ts +5 -0
- package/domains/datastores/resources.d.ts.map +1 -0
- package/domains/datastores/resources.js +16 -0
- package/domains/datastores/resources.js.map +1 -0
- package/domains/datastores/tools.d.ts +5 -0
- package/domains/datastores/tools.d.ts.map +1 -0
- package/domains/datastores/tools.js +46 -0
- package/domains/datastores/tools.js.map +1 -0
- package/domains/environments/apply.d.ts +4 -0
- package/domains/environments/apply.d.ts.map +1 -0
- package/domains/environments/apply.js +30 -0
- package/domains/environments/apply.js.map +1 -0
- package/domains/environments/delete.d.ts +3 -0
- package/domains/environments/delete.d.ts.map +1 -0
- package/domains/environments/delete.js +36 -0
- package/domains/environments/delete.js.map +1 -0
- package/domains/environments/fetch.d.ts +6 -0
- package/domains/environments/fetch.d.ts.map +1 -0
- package/domains/environments/fetch.js +30 -0
- package/domains/environments/fetch.js.map +1 -0
- package/domains/environments/resources.d.ts +5 -0
- package/domains/environments/resources.d.ts.map +1 -0
- package/domains/environments/resources.js +16 -0
- package/domains/environments/resources.js.map +1 -0
- package/domains/environments/tools.d.ts +5 -0
- package/domains/environments/tools.d.ts.map +1 -0
- package/domains/environments/tools.js +51 -0
- package/domains/environments/tools.js.map +1 -0
- package/domains/executions/cancel.d.ts +9 -0
- package/domains/executions/cancel.d.ts.map +1 -0
- package/domains/executions/cancel.js +114 -0
- package/domains/executions/cancel.js.map +1 -0
- package/domains/executions/tools.d.ts +5 -0
- package/domains/executions/tools.d.ts.map +1 -0
- package/domains/executions/tools.js +28 -0
- package/domains/executions/tools.js.map +1 -0
- 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 +139 -0
- package/domains/records/tools.js.map +1 -0
- package/domains/resourceuri.d.ts.map +1 -1
- package/domains/resourceuri.js +2 -0
- package/domains/resourceuri.js.map +1 -1
- package/domains/search/tools.d.ts.map +1 -1
- package/domains/search/tools.js +10 -7
- package/domains/search/tools.js.map +1 -1
- package/domains/skills/tools.d.ts.map +1 -1
- package/domains/skills/tools.js +25 -1
- package/domains/skills/tools.js.map +1 -1
- package/domains/skills/versions.d.ts +9 -0
- package/domains/skills/versions.d.ts.map +1 -0
- package/domains/skills/versions.js +31 -0
- package/domains/skills/versions.js.map +1 -0
- package/domains/workflowexecutions/approvals.d.ts +17 -0
- package/domains/workflowexecutions/approvals.d.ts.map +1 -0
- package/domains/workflowexecutions/approvals.js +61 -0
- package/domains/workflowexecutions/approvals.js.map +1 -0
- package/domains/workflowexecutions/run.d.ts +12 -0
- package/domains/workflowexecutions/run.d.ts.map +1 -0
- package/domains/workflowexecutions/run.js +69 -0
- package/domains/workflowexecutions/run.js.map +1 -0
- package/domains/workflowexecutions/tools.d.ts.map +1 -1
- package/domains/workflowexecutions/tools.js +89 -6
- package/domains/workflowexecutions/tools.js.map +1 -1
- package/domains/workflows/tools.d.ts.map +1 -1
- package/domains/workflows/tools.js +69 -5
- package/domains/workflows/tools.js.map +1 -1
- package/domains/workflows/versions.d.ts +29 -0
- package/domains/workflows/versions.d.ts.map +1 -0
- package/domains/workflows/versions.js +99 -0
- package/domains/workflows/versions.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/datastore.d.ts +861 -0
- package/gen/datastore.d.ts.map +1 -0
- package/gen/datastore.js +267 -0
- package/gen/datastore.js.map +1 -0
- package/gen/environment.d.ts +77 -0
- package/gen/environment.d.ts.map +1 -0
- package/gen/environment.js +62 -0
- package/gen/environment.js.map +1 -0
- package/gen/workflow.d.ts +4 -4
- 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 +53 -3
- package/server.js.map +1 -1
- package/src/config.ts +19 -0
- package/src/domains/agentexecutions/approve.ts +67 -0
- package/src/domains/agentexecutions/fetch.ts +112 -0
- package/src/domains/agentexecutions/run.ts +112 -0
- package/src/domains/agentexecutions/tools.ts +139 -0
- package/src/domains/apply.integration.test.ts +82 -6
- package/src/domains/datastores/apply.ts +33 -0
- package/src/domains/datastores/delete.ts +47 -0
- package/src/domains/datastores/fetch.ts +32 -0
- package/src/domains/datastores/resources.ts +21 -0
- package/src/domains/datastores/tools.ts +76 -0
- package/src/domains/deletes.integration.test.ts +49 -2
- package/src/domains/environments/apply.ts +40 -0
- package/src/domains/environments/delete.ts +45 -0
- package/src/domains/environments/fetch.ts +39 -0
- package/src/domains/environments/resources.ts +21 -0
- package/src/domains/environments/tools.ts +81 -0
- package/src/domains/executions/cancel.ts +148 -0
- package/src/domains/executions/tools.ts +45 -0
- package/src/domains/executions.integration.test.ts +376 -0
- package/src/domains/reads.integration.test.ts +51 -5
- 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 +210 -0
- package/src/domains/resources.integration.test.ts +38 -2
- package/src/domains/resourceuri.test.ts +11 -2
- package/src/domains/resourceuri.ts +2 -0
- package/src/domains/search/search.integration.test.ts +18 -2
- package/src/domains/search/tools.ts +12 -7
- package/src/domains/skills/tools.ts +34 -1
- package/src/domains/skills/versions.ts +47 -0
- package/src/domains/versions.integration.test.ts +212 -0
- package/src/domains/workflowexecutions/approvals.ts +102 -0
- package/src/domains/workflowexecutions/run.ts +87 -0
- package/src/domains/workflowexecutions/tools.ts +120 -6
- package/src/domains/workflows/tools.ts +90 -7
- package/src/domains/workflows/versions.ts +147 -0
- package/src/gen/agent.ts +28 -1
- package/src/gen/datastore.ts +264 -0
- package/src/gen/environment.ts +66 -0
- package/src/http.integration.test.ts +1 -0
- package/src/index.ts +12 -5
- package/src/server.ts +71 -5
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// In-process integration test for the execution-loop tools: run_agent,
|
|
2
|
+
// run_workflow, get_agent_execution, the two approval tools,
|
|
3
|
+
// list_pending_approvals, and cancel_execution.
|
|
4
|
+
//
|
|
5
|
+
// Same harness as reads.integration.test.ts: a real Connect backend serving
|
|
6
|
+
// stubbed controllers, the MCP server driven through an in-memory client. The
|
|
7
|
+
// stubs capture requests so the tests assert the exact protos the tools send
|
|
8
|
+
// (slug→ID resolution, runtime-env conversion, approval enum mapping) and
|
|
9
|
+
// script execution state (running vs terminal, long message histories) to
|
|
10
|
+
// exercise the compact projection and the cancel short-circuit.
|
|
11
|
+
|
|
12
|
+
import { create, toJson } from "@bufbuild/protobuf";
|
|
13
|
+
import type { ConnectRouter } from "@connectrpc/connect";
|
|
14
|
+
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
15
|
+
import {
|
|
16
|
+
createServer as createHttp2Server,
|
|
17
|
+
type Http2Server,
|
|
18
|
+
type ServerHttp2Session,
|
|
19
|
+
} from "node:http2";
|
|
20
|
+
import type { AddressInfo } from "node:net";
|
|
21
|
+
|
|
22
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
23
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
24
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
25
|
+
import { AgentQueryController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/query_pb";
|
|
26
|
+
import {
|
|
27
|
+
AgentExecutionSchema,
|
|
28
|
+
type AgentExecution,
|
|
29
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
30
|
+
import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
|
|
31
|
+
import {
|
|
32
|
+
ApprovalAction,
|
|
33
|
+
ExecutionPhase,
|
|
34
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
35
|
+
import { AgentExecutionQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/query_pb";
|
|
36
|
+
import type { SubmitApprovalInput } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
|
|
37
|
+
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
38
|
+
import { WorkflowQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/query_pb";
|
|
39
|
+
import {
|
|
40
|
+
WorkflowExecutionSchema,
|
|
41
|
+
type WorkflowExecution,
|
|
42
|
+
} from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
|
|
43
|
+
import { WorkflowExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/command_pb";
|
|
44
|
+
import { ExecutionPhase as WorkflowExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
|
|
45
|
+
import {
|
|
46
|
+
PendingApprovalsListSchema,
|
|
47
|
+
type ListPendingApprovalsRequest,
|
|
48
|
+
type SubmitWorkflowTaskApprovalInput,
|
|
49
|
+
} from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/io_pb";
|
|
50
|
+
import { WorkflowExecutionQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/query_pb";
|
|
51
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
52
|
+
|
|
53
|
+
import { configureLogger } from "../logger";
|
|
54
|
+
import { createServer } from "../server";
|
|
55
|
+
|
|
56
|
+
configureLogger({ level: "error", format: "text" });
|
|
57
|
+
|
|
58
|
+
const knownAgent = create(AgentSchema, {
|
|
59
|
+
apiVersion: "v1",
|
|
60
|
+
kind: "agent",
|
|
61
|
+
metadata: { name: "Code Reviewer", slug: "code-reviewer", org: "acme", id: "agt_1" },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const knownWorkflow = create(WorkflowSchema, {
|
|
65
|
+
apiVersion: "v1",
|
|
66
|
+
kind: "workflow",
|
|
67
|
+
metadata: { name: "Release", slug: "release", org: "acme", id: "wkf_1" },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** An execution with a scriptable phase and an 8-message history. */
|
|
71
|
+
function agentExecutionFixture(phase: ExecutionPhase): AgentExecution {
|
|
72
|
+
return create(AgentExecutionSchema, {
|
|
73
|
+
apiVersion: "v1",
|
|
74
|
+
kind: "AgentExecution",
|
|
75
|
+
metadata: { name: "run", org: "acme", id: "aex_1" },
|
|
76
|
+
spec: { agentId: "agt_1", sessionId: "ses_1", message: "review this" },
|
|
77
|
+
status: {
|
|
78
|
+
phase,
|
|
79
|
+
messages: Array.from({ length: 8 }, (_, i) => ({ content: `msg-${i}` })),
|
|
80
|
+
// Bulk fields the compact view must prune.
|
|
81
|
+
subAgentExecutions: [{ name: "researcher", input: "dig into the logs" }],
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function workflowExecutionFixture(phase: WorkflowExecutionPhase): WorkflowExecution {
|
|
87
|
+
return create(WorkflowExecutionSchema, {
|
|
88
|
+
apiVersion: "v1",
|
|
89
|
+
kind: "WorkflowExecution",
|
|
90
|
+
metadata: { name: "run", org: "acme", id: "wex_1" },
|
|
91
|
+
spec: { workflowId: "wkf_1" },
|
|
92
|
+
status: { phase },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const pendingApprovals = create(PendingApprovalsListSchema, {
|
|
97
|
+
entries: [{ executionId: "wex_1", workflowName: "Release", taskName: "sign-off" }],
|
|
98
|
+
totalCount: 1,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let backend: Http2Server;
|
|
102
|
+
let client: Client;
|
|
103
|
+
const openSessions = new Set<ServerHttp2Session>();
|
|
104
|
+
|
|
105
|
+
// Captured requests / scripted state, reset per test.
|
|
106
|
+
let createdAgentExecution: AgentExecution | undefined;
|
|
107
|
+
let createdWorkflowExecution: WorkflowExecution | undefined;
|
|
108
|
+
let agentExecutionState: AgentExecution;
|
|
109
|
+
let workflowExecutionState: WorkflowExecution;
|
|
110
|
+
let lastAgentApproval: SubmitApprovalInput | undefined;
|
|
111
|
+
let lastWorkflowApproval: SubmitWorkflowTaskApprovalInput | undefined;
|
|
112
|
+
let lastPendingApprovalsRequest: ListPendingApprovalsRequest | undefined;
|
|
113
|
+
let agentCancelCalls = 0;
|
|
114
|
+
let workflowCancelCalls = 0;
|
|
115
|
+
|
|
116
|
+
interface ToolResult {
|
|
117
|
+
content: Array<{ type: string; text?: string }>;
|
|
118
|
+
isError?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function callTool(name: string, args: Record<string, unknown>): Promise<ToolResult> {
|
|
122
|
+
return (await client.callTool({ name, arguments: args })) as ToolResult;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseText(result: ToolResult): Record<string, unknown> {
|
|
126
|
+
return JSON.parse(result.content[0]?.text ?? "{}") as Record<string, unknown>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
beforeAll(async () => {
|
|
130
|
+
const routes = (router: ConnectRouter) => {
|
|
131
|
+
router.service(AgentQueryController, { getByReference: () => knownAgent });
|
|
132
|
+
router.service(AgentExecutionQueryController, {
|
|
133
|
+
get: () => agentExecutionState,
|
|
134
|
+
});
|
|
135
|
+
router.service(AgentExecutionCommandController, {
|
|
136
|
+
create: (req) => {
|
|
137
|
+
createdAgentExecution = req;
|
|
138
|
+
return agentExecutionFixture(ExecutionPhase.EXECUTION_PENDING);
|
|
139
|
+
},
|
|
140
|
+
submitApproval: (req) => {
|
|
141
|
+
lastAgentApproval = req;
|
|
142
|
+
return agentExecutionFixture(ExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
143
|
+
},
|
|
144
|
+
cancel: () => {
|
|
145
|
+
agentCancelCalls++;
|
|
146
|
+
return agentExecutionFixture(ExecutionPhase.EXECUTION_CANCELLED);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
router.service(WorkflowQueryController, { getByReference: () => knownWorkflow });
|
|
150
|
+
router.service(WorkflowExecutionQueryController, {
|
|
151
|
+
get: () => workflowExecutionState,
|
|
152
|
+
listPendingApprovals: (req) => {
|
|
153
|
+
lastPendingApprovalsRequest = req;
|
|
154
|
+
return pendingApprovals;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
router.service(WorkflowExecutionCommandController, {
|
|
158
|
+
create: (req) => {
|
|
159
|
+
createdWorkflowExecution = req;
|
|
160
|
+
return workflowExecutionFixture(WorkflowExecutionPhase.EXECUTION_PENDING);
|
|
161
|
+
},
|
|
162
|
+
submitWorkflowTaskApproval: (req) => {
|
|
163
|
+
lastWorkflowApproval = req;
|
|
164
|
+
return workflowExecutionFixture(WorkflowExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
165
|
+
},
|
|
166
|
+
cancel: () => {
|
|
167
|
+
workflowCancelCalls++;
|
|
168
|
+
return workflowExecutionFixture(WorkflowExecutionPhase.EXECUTION_CANCELLED);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
173
|
+
backend.on("session", (session) => {
|
|
174
|
+
openSessions.add(session);
|
|
175
|
+
session.on("close", () => openSessions.delete(session));
|
|
176
|
+
});
|
|
177
|
+
await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
|
|
178
|
+
const port = (backend.address() as AddressInfo).port;
|
|
179
|
+
|
|
180
|
+
const mcp = createServer({ serverAddress: `127.0.0.1:${port}`, apiKey: "" });
|
|
181
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
182
|
+
client = new Client({ name: "executions-integration", version: "test" });
|
|
183
|
+
await Promise.all([mcp.connect(serverTransport), client.connect(clientTransport)]);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
createdAgentExecution = undefined;
|
|
188
|
+
createdWorkflowExecution = undefined;
|
|
189
|
+
agentExecutionState = agentExecutionFixture(ExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
190
|
+
workflowExecutionState = workflowExecutionFixture(WorkflowExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
191
|
+
lastAgentApproval = undefined;
|
|
192
|
+
lastWorkflowApproval = undefined;
|
|
193
|
+
lastPendingApprovalsRequest = undefined;
|
|
194
|
+
agentCancelCalls = 0;
|
|
195
|
+
workflowCancelCalls = 0;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
afterAll(async () => {
|
|
199
|
+
await client?.close();
|
|
200
|
+
for (const session of openSessions) session.destroy();
|
|
201
|
+
await new Promise<void>((resolve) => backend.close(() => resolve()));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("execution tools integration", () => {
|
|
205
|
+
it("advertises the execution-loop tools", async () => {
|
|
206
|
+
const { tools } = await client.listTools();
|
|
207
|
+
expect(tools.map((t) => t.name)).toEqual(
|
|
208
|
+
expect.arrayContaining([
|
|
209
|
+
"run_agent",
|
|
210
|
+
"run_workflow",
|
|
211
|
+
"get_agent_execution",
|
|
212
|
+
"submit_agent_execution_approval",
|
|
213
|
+
"list_pending_approvals",
|
|
214
|
+
"submit_workflow_task_approval",
|
|
215
|
+
"cancel_execution",
|
|
216
|
+
]),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("run_agent resolves the slug and creates the execution", async () => {
|
|
221
|
+
const result = await callTool("run_agent", {
|
|
222
|
+
org: "acme",
|
|
223
|
+
agent: "code-reviewer",
|
|
224
|
+
message: "review this PR",
|
|
225
|
+
runtime_env: { REPO: "stigmer/stigmer" },
|
|
226
|
+
});
|
|
227
|
+
expect(result.isError).toBeFalsy();
|
|
228
|
+
|
|
229
|
+
expect(createdAgentExecution?.spec?.agentId).toBe("agt_1");
|
|
230
|
+
expect(createdAgentExecution?.spec?.message).toBe("review this PR");
|
|
231
|
+
expect(createdAgentExecution?.spec?.sessionId).toBe("");
|
|
232
|
+
expect(createdAgentExecution?.metadata?.org).toBe("acme");
|
|
233
|
+
// Runtime env values through MCP are never secrets.
|
|
234
|
+
expect(createdAgentExecution?.spec?.runtimeEnv?.REPO?.value).toBe("stigmer/stigmer");
|
|
235
|
+
expect(createdAgentExecution?.spec?.runtimeEnv?.REPO?.isSecret).toBe(false);
|
|
236
|
+
|
|
237
|
+
// The created execution comes back as plain protojson (its status is small).
|
|
238
|
+
const created = parseText(result);
|
|
239
|
+
expect((created.metadata as Record<string, unknown>).id).toBe("aex_1");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("run_agent threads a session follow-up", async () => {
|
|
243
|
+
await callTool("run_agent", {
|
|
244
|
+
org: "acme",
|
|
245
|
+
agent: "code-reviewer",
|
|
246
|
+
message: "and the tests?",
|
|
247
|
+
session_id: "ses_42",
|
|
248
|
+
});
|
|
249
|
+
expect(createdAgentExecution?.spec?.sessionId).toBe("ses_42");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("run_workflow creates the execution with the org env injected", async () => {
|
|
253
|
+
const result = await callTool("run_workflow", { org: "acme", workflow: "release" });
|
|
254
|
+
expect(result.isError).toBeFalsy();
|
|
255
|
+
|
|
256
|
+
expect(createdWorkflowExecution?.spec?.workflowId).toBe("wkf_1");
|
|
257
|
+
expect(createdWorkflowExecution?.spec?.triggerMessage).toBe("execute");
|
|
258
|
+
// The CLI-parity org injection.
|
|
259
|
+
expect(createdWorkflowExecution?.spec?.runtimeEnv?.STIGMER_ORG_ID?.value).toBe("acme");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("run_workflow lets a caller-supplied STIGMER_ORG_ID win", async () => {
|
|
263
|
+
await callTool("run_workflow", {
|
|
264
|
+
org: "acme",
|
|
265
|
+
workflow: "release",
|
|
266
|
+
message: "ship it",
|
|
267
|
+
runtime_env: { STIGMER_ORG_ID: "other-org" },
|
|
268
|
+
});
|
|
269
|
+
expect(createdWorkflowExecution?.spec?.triggerMessage).toBe("ship it");
|
|
270
|
+
expect(createdWorkflowExecution?.spec?.runtimeEnv?.STIGMER_ORG_ID?.value).toBe("other-org");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("get_agent_execution defaults to the compact view", async () => {
|
|
274
|
+
const result = await callTool("get_agent_execution", { execution_id: "aex_1" });
|
|
275
|
+
expect(result.isError).toBeFalsy();
|
|
276
|
+
|
|
277
|
+
const body = parseText(result);
|
|
278
|
+
expect(body.view).toBe("compact");
|
|
279
|
+
expect(body.total_messages).toBe(8);
|
|
280
|
+
|
|
281
|
+
const execution = body.execution as Record<string, unknown>;
|
|
282
|
+
const status = (execution.status ?? {}) as Record<string, unknown>;
|
|
283
|
+
const messages = status.messages as Array<Record<string, unknown>>;
|
|
284
|
+
// Default tail of 5, ending with the newest message.
|
|
285
|
+
expect(messages).toHaveLength(5);
|
|
286
|
+
expect(messages[4]?.content).toBe("msg-7");
|
|
287
|
+
// Bulk bookkeeping fields are pruned.
|
|
288
|
+
expect(status.sub_agent_executions).toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("get_agent_execution honors message_limit", async () => {
|
|
292
|
+
const body = parseText(
|
|
293
|
+
await callTool("get_agent_execution", { execution_id: "aex_1", message_limit: 2 }),
|
|
294
|
+
);
|
|
295
|
+
const status = ((body.execution as Record<string, unknown>).status ?? {}) as Record<
|
|
296
|
+
string,
|
|
297
|
+
unknown
|
|
298
|
+
>;
|
|
299
|
+
expect(status.messages as unknown[]).toHaveLength(2);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("get_agent_execution view=full returns the backend protojson verbatim", async () => {
|
|
303
|
+
const result = await callTool("get_agent_execution", { execution_id: "aex_1", view: "full" });
|
|
304
|
+
expect(parseText(result)).toEqual(
|
|
305
|
+
toJson(AgentExecutionSchema, agentExecutionState, { useProtoFieldName: true }),
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("submit_agent_execution_approval maps the action and returns the compact view", async () => {
|
|
310
|
+
const result = await callTool("submit_agent_execution_approval", {
|
|
311
|
+
execution_id: "aex_1",
|
|
312
|
+
tool_call_id: "call_7",
|
|
313
|
+
action: "reject",
|
|
314
|
+
comment: "wrong repository",
|
|
315
|
+
});
|
|
316
|
+
expect(result.isError).toBeFalsy();
|
|
317
|
+
expect(lastAgentApproval?.agentExecutionId).toBe("aex_1");
|
|
318
|
+
expect(lastAgentApproval?.toolCallId).toBe("call_7");
|
|
319
|
+
expect(lastAgentApproval?.action).toBe(ApprovalAction.REJECT);
|
|
320
|
+
expect(lastAgentApproval?.comment).toBe("wrong repository");
|
|
321
|
+
expect(parseText(result).view).toBe("compact");
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("list_pending_approvals forwards the org and returns the inbox", async () => {
|
|
325
|
+
const result = await callTool("list_pending_approvals", { org: "acme" });
|
|
326
|
+
expect(result.isError).toBeFalsy();
|
|
327
|
+
expect(lastPendingApprovalsRequest?.org).toBe("acme");
|
|
328
|
+
expect(parseText(result)).toEqual(
|
|
329
|
+
toJson(PendingApprovalsListSchema, pendingApprovals, { useProtoFieldName: true }),
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("submit_workflow_task_approval forwards the decision and form data", async () => {
|
|
334
|
+
const result = await callTool("submit_workflow_task_approval", {
|
|
335
|
+
execution_id: "wex_1",
|
|
336
|
+
task_name: "sign-off",
|
|
337
|
+
outcome: "approve",
|
|
338
|
+
comment: "lgtm",
|
|
339
|
+
form_data: { severity: "low" },
|
|
340
|
+
});
|
|
341
|
+
expect(result.isError).toBeFalsy();
|
|
342
|
+
expect(lastWorkflowApproval?.executionId).toBe("wex_1");
|
|
343
|
+
expect(lastWorkflowApproval?.taskName).toBe("sign-off");
|
|
344
|
+
expect(lastWorkflowApproval?.outcome).toBe("approve");
|
|
345
|
+
expect(lastWorkflowApproval?.comment).toBe("lgtm");
|
|
346
|
+
expect(lastWorkflowApproval?.formData).toMatchObject({ severity: "low" });
|
|
347
|
+
// The reviewer field is server-attributed; interactive clients must not set it.
|
|
348
|
+
expect(lastWorkflowApproval?.reviewer).toBe("");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("cancel_execution cancels a running agent execution", async () => {
|
|
352
|
+
const body = parseText(await callTool("cancel_execution", { execution_id: "aex_1" }));
|
|
353
|
+
expect(agentCancelCalls).toBe(1);
|
|
354
|
+
expect(body.already_terminal).toBe(false);
|
|
355
|
+
expect(body.view).toBe("compact");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("cancel_execution short-circuits a terminal agent execution", async () => {
|
|
359
|
+
agentExecutionState = agentExecutionFixture(ExecutionPhase.EXECUTION_COMPLETED);
|
|
360
|
+
const body = parseText(await callTool("cancel_execution", { execution_id: "aex_1" }));
|
|
361
|
+
expect(agentCancelCalls).toBe(0);
|
|
362
|
+
expect(body.already_terminal).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("cancel_execution routes workflow executions by prefix", async () => {
|
|
366
|
+
const body = parseText(await callTool("cancel_execution", { execution_id: "wex_1" }));
|
|
367
|
+
expect(workflowCancelCalls).toBe(1);
|
|
368
|
+
expect(body.already_terminal).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("cancel_execution rejects an unrecognized ID format", async () => {
|
|
372
|
+
const result = await callTool("cancel_execution", { execution_id: "ses_123" });
|
|
373
|
+
expect(result.isError).toBe(true);
|
|
374
|
+
expect(result.content[0]?.text).toContain("unrecognized execution ID format");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// In-process integration test for the read tools (get_mcp_server, get_skill,
|
|
2
|
-
// get_workflow). Stands up a real Connect
|
|
3
|
-
// controllers, drives the MCP server through an
|
|
4
|
-
// each tool returns the backend's protojson
|
|
5
|
-
// that get_skill forwards its optional
|
|
2
|
+
// get_workflow, get_environment, get_datastore). Stands up a real Connect
|
|
3
|
+
// backend serving the query controllers, drives the MCP server through an
|
|
4
|
+
// in-memory client, and asserts each tool returns the backend's protojson
|
|
5
|
+
// verbatim (the parity contract) and that get_skill forwards its optional
|
|
6
|
+
// version to the backend.
|
|
6
7
|
|
|
7
8
|
import { create, toJson } from "@bufbuild/protobuf";
|
|
8
9
|
import type { ConnectRouter } from "@connectrpc/connect";
|
|
@@ -16,6 +17,10 @@ import type { AddressInfo } from "node:net";
|
|
|
16
17
|
|
|
17
18
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
18
19
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
20
|
+
import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
|
|
21
|
+
import { DatastoreQueryController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/query_pb";
|
|
22
|
+
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
23
|
+
import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
|
|
19
24
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
20
25
|
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
21
26
|
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
@@ -47,6 +52,21 @@ const knownWorkflow = create(WorkflowSchema, {
|
|
|
47
52
|
metadata: { name: "Release", slug: "release", org: "acme", id: "wkf-1" },
|
|
48
53
|
});
|
|
49
54
|
|
|
55
|
+
// The backend redacts secret values before they leave the server; the tool
|
|
56
|
+
// must pass that redaction through verbatim.
|
|
57
|
+
const knownEnvironment = create(EnvironmentSchema, {
|
|
58
|
+
apiVersion: "v1",
|
|
59
|
+
kind: "environment",
|
|
60
|
+
metadata: { name: "GitHub Creds", slug: "github-creds", org: "acme", id: "env-1" },
|
|
61
|
+
spec: { data: { API_KEY: { value: "***REDACTED***", isSecret: true } } },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const knownDatastore = create(DatastoreSchema, {
|
|
65
|
+
apiVersion: "v1",
|
|
66
|
+
kind: "datastore",
|
|
67
|
+
metadata: { name: "Bookings", slug: "bookings", org: "acme", id: "dst-1" },
|
|
68
|
+
});
|
|
69
|
+
|
|
50
70
|
let backend: Http2Server;
|
|
51
71
|
let client: Client;
|
|
52
72
|
let lastSkillVersion: string | undefined;
|
|
@@ -71,6 +91,8 @@ beforeAll(async () => {
|
|
|
71
91
|
},
|
|
72
92
|
});
|
|
73
93
|
router.service(WorkflowQueryController, { getByReference: () => knownWorkflow });
|
|
94
|
+
router.service(EnvironmentQueryController, { getByReference: () => knownEnvironment });
|
|
95
|
+
router.service(DatastoreQueryController, { getByReference: () => knownDatastore });
|
|
74
96
|
};
|
|
75
97
|
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
76
98
|
// Force keep-alive sessions closed on teardown, else backend.close() blocks.
|
|
@@ -97,7 +119,14 @@ describe("read tools integration", () => {
|
|
|
97
119
|
it("advertises all read tools", async () => {
|
|
98
120
|
const { tools } = await client.listTools();
|
|
99
121
|
expect(tools.map((t) => t.name)).toEqual(
|
|
100
|
-
expect.arrayContaining([
|
|
122
|
+
expect.arrayContaining([
|
|
123
|
+
"get_agent",
|
|
124
|
+
"get_mcp_server",
|
|
125
|
+
"get_skill",
|
|
126
|
+
"get_workflow",
|
|
127
|
+
"get_environment",
|
|
128
|
+
"get_datastore",
|
|
129
|
+
]),
|
|
101
130
|
);
|
|
102
131
|
});
|
|
103
132
|
|
|
@@ -131,4 +160,21 @@ describe("read tools integration", () => {
|
|
|
131
160
|
await callTool("get_skill", { org: "acme", slug: "code-review", version: "stable" });
|
|
132
161
|
expect(lastSkillVersion).toBe("stable");
|
|
133
162
|
});
|
|
163
|
+
|
|
164
|
+
it("get_environment passes the server's secret redaction through verbatim", async () => {
|
|
165
|
+
const result = await callTool("get_environment", { org: "acme", slug: "github-creds" });
|
|
166
|
+
expect(result.isError).toBeFalsy();
|
|
167
|
+
const body = JSON.parse(result.content[0]?.text ?? "{}") as Record<string, unknown>;
|
|
168
|
+
expect(body).toEqual(toJson(EnvironmentSchema, knownEnvironment, { useProtoFieldName: true }));
|
|
169
|
+
const data = (body.spec as { data: Record<string, { value: string }> }).data;
|
|
170
|
+
expect(data.API_KEY?.value).toBe("***REDACTED***");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("get_datastore returns the backend protojson", async () => {
|
|
174
|
+
const result = await callTool("get_datastore", { org: "acme", slug: "bookings" });
|
|
175
|
+
expect(result.isError).toBeFalsy();
|
|
176
|
+
expect(JSON.parse(result.content[0]?.text ?? "{}")).toEqual(
|
|
177
|
+
toJson(DatastoreSchema, knownDatastore, { useProtoFieldName: true }),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
134
180
|
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Record-RPC invocations for the five record tools — 1:1 projections
|
|
2
|
+
// of DatastoreRecordQueryController / DatastoreRecordCommandController
|
|
3
|
+
// (DD-005: the tool layer adds no semantics; validation, grants,
|
|
4
|
+
// scopes, constraints, and messages all live in record-RPC domain
|
|
5
|
+
// logic on the control plane).
|
|
6
|
+
//
|
|
7
|
+
// One file for the five thin calls rather than the resource domains'
|
|
8
|
+
// file-per-verb split: resource verbs each carry orchestration
|
|
9
|
+
// (reference resolution, YAML parsing, two-step deletes); record calls
|
|
10
|
+
// are each "build request, call, marshal" and the shared argument
|
|
11
|
+
// mapping (filter/value building) is the only substance.
|
|
12
|
+
|
|
13
|
+
import { create, fromJson, type JsonObject } from "@bufbuild/protobuf";
|
|
14
|
+
import { ValueSchema, type Value } from "@bufbuild/protobuf/wkt";
|
|
15
|
+
import {
|
|
16
|
+
DatastoreRecordCommandController,
|
|
17
|
+
} from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_command_pb";
|
|
18
|
+
import {
|
|
19
|
+
DatastoreRecordQueryController,
|
|
20
|
+
} from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_query_pb";
|
|
21
|
+
import {
|
|
22
|
+
DatastoreDescriptionSchema,
|
|
23
|
+
DeleteRecordRequestSchema,
|
|
24
|
+
DescribeDatastoreRequestSchema,
|
|
25
|
+
FindRecordsRequestSchema,
|
|
26
|
+
InsertRecordRequestSchema,
|
|
27
|
+
RecordConditionOp,
|
|
28
|
+
RecordConditionSchema,
|
|
29
|
+
RecordEnvelopeSchema,
|
|
30
|
+
RecordFilterSchema,
|
|
31
|
+
RecordListSchema,
|
|
32
|
+
RecordOrderBySchema,
|
|
33
|
+
RecordSortDirection,
|
|
34
|
+
UpdateRecordRequestSchema,
|
|
35
|
+
type RecordCondition,
|
|
36
|
+
type RecordFilter,
|
|
37
|
+
type RecordOrderBy,
|
|
38
|
+
} from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/record_io_pb";
|
|
39
|
+
import { withClient } from "../client.js";
|
|
40
|
+
import { toProtoJson } from "../marshal.js";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A filter condition as the model supplies it. `op` uses the natural
|
|
44
|
+
* spelling "in" (the proto enum's `is_in` exists only because `in` is
|
|
45
|
+
* reserved in Java/Python — a substrate detail the model never sees).
|
|
46
|
+
*/
|
|
47
|
+
export interface ConditionArg {
|
|
48
|
+
field: string;
|
|
49
|
+
op: string;
|
|
50
|
+
value?: string | number | boolean | null;
|
|
51
|
+
values?: (string | number | boolean)[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface FindArgs {
|
|
55
|
+
datastore: string;
|
|
56
|
+
org?: string;
|
|
57
|
+
collection: string;
|
|
58
|
+
conditions?: ConditionArg[];
|
|
59
|
+
order_by?: { field: string; direction?: string };
|
|
60
|
+
limit?: number;
|
|
61
|
+
offset?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Model-facing operator spelling → proto enum. */
|
|
65
|
+
const CONDITION_OPS: Readonly<Record<string, RecordConditionOp>> = {
|
|
66
|
+
eq: RecordConditionOp.eq,
|
|
67
|
+
neq: RecordConditionOp.neq,
|
|
68
|
+
gt: RecordConditionOp.gt,
|
|
69
|
+
gte: RecordConditionOp.gte,
|
|
70
|
+
lt: RecordConditionOp.lt,
|
|
71
|
+
lte: RecordConditionOp.lte,
|
|
72
|
+
in: RecordConditionOp.is_in,
|
|
73
|
+
not_in: RecordConditionOp.not_in,
|
|
74
|
+
is_null: RecordConditionOp.is_null,
|
|
75
|
+
not_null: RecordConditionOp.not_null,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export async function findRecords(
|
|
79
|
+
serverAddress: string,
|
|
80
|
+
token: string,
|
|
81
|
+
args: FindArgs,
|
|
82
|
+
): Promise<string> {
|
|
83
|
+
const request = create(FindRecordsRequestSchema, {
|
|
84
|
+
datastore: args.datastore,
|
|
85
|
+
org: args.org ?? "",
|
|
86
|
+
collection: args.collection,
|
|
87
|
+
filter: buildFilter(args.conditions),
|
|
88
|
+
orderBy: buildOrderBy(args.order_by),
|
|
89
|
+
limit: args.limit ?? 0,
|
|
90
|
+
offset: args.offset ?? 0,
|
|
91
|
+
});
|
|
92
|
+
return withClient(DatastoreRecordQueryController, serverAddress, token, async (client, opts) =>
|
|
93
|
+
toProtoJson(RecordListSchema, await client.findRecords(request, opts)),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function insertRecord(
|
|
98
|
+
serverAddress: string,
|
|
99
|
+
token: string,
|
|
100
|
+
args: { datastore: string; org?: string; collection: string; record: Record<string, unknown> },
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
const request = create(InsertRecordRequestSchema, {
|
|
103
|
+
datastore: args.datastore,
|
|
104
|
+
org: args.org ?? "",
|
|
105
|
+
collection: args.collection,
|
|
106
|
+
// google.protobuf.Struct fields are plain JSON objects in
|
|
107
|
+
// protobuf-es v2 — no conversion.
|
|
108
|
+
record: args.record as JsonObject,
|
|
109
|
+
});
|
|
110
|
+
return withClient(DatastoreRecordCommandController, serverAddress, token, async (client, opts) =>
|
|
111
|
+
toProtoJson(RecordEnvelopeSchema, await client.insertRecord(request, opts)),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function updateRecord(
|
|
116
|
+
serverAddress: string,
|
|
117
|
+
token: string,
|
|
118
|
+
args: {
|
|
119
|
+
datastore: string;
|
|
120
|
+
org?: string;
|
|
121
|
+
collection: string;
|
|
122
|
+
id: string;
|
|
123
|
+
fields: Record<string, unknown>;
|
|
124
|
+
},
|
|
125
|
+
): Promise<string> {
|
|
126
|
+
const request = create(UpdateRecordRequestSchema, {
|
|
127
|
+
datastore: args.datastore,
|
|
128
|
+
org: args.org ?? "",
|
|
129
|
+
collection: args.collection,
|
|
130
|
+
id: args.id,
|
|
131
|
+
fields: args.fields as JsonObject,
|
|
132
|
+
});
|
|
133
|
+
return withClient(DatastoreRecordCommandController, serverAddress, token, async (client, opts) =>
|
|
134
|
+
toProtoJson(RecordEnvelopeSchema, await client.updateRecord(request, opts)),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function deleteRecord(
|
|
139
|
+
serverAddress: string,
|
|
140
|
+
token: string,
|
|
141
|
+
args: { datastore: string; org?: string; collection: string; id: string },
|
|
142
|
+
): Promise<string> {
|
|
143
|
+
const request = create(DeleteRecordRequestSchema, {
|
|
144
|
+
datastore: args.datastore,
|
|
145
|
+
org: args.org ?? "",
|
|
146
|
+
collection: args.collection,
|
|
147
|
+
id: args.id,
|
|
148
|
+
});
|
|
149
|
+
return withClient(DatastoreRecordCommandController, serverAddress, token, async (client, opts) =>
|
|
150
|
+
toProtoJson(RecordEnvelopeSchema, await client.deleteRecord(request, opts)),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function describeDatastore(
|
|
155
|
+
serverAddress: string,
|
|
156
|
+
token: string,
|
|
157
|
+
args: { datastore: string; org?: string },
|
|
158
|
+
): Promise<string> {
|
|
159
|
+
const request = create(DescribeDatastoreRequestSchema, {
|
|
160
|
+
datastore: args.datastore,
|
|
161
|
+
org: args.org ?? "",
|
|
162
|
+
});
|
|
163
|
+
return withClient(DatastoreRecordQueryController, serverAddress, token, async (client, opts) =>
|
|
164
|
+
toProtoJson(DatastoreDescriptionSchema, await client.describeDatastore(request, opts)),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build the typed AND-only filter (DD-005 SD-2). An unknown operator is
|
|
170
|
+
* left unspecified for the server to reject with its own domain
|
|
171
|
+
* message — the bridge never fabricates validation of its own.
|
|
172
|
+
*/
|
|
173
|
+
function buildFilter(conditions: ConditionArg[] | undefined): RecordFilter | undefined {
|
|
174
|
+
if (conditions === undefined || conditions.length === 0) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
return create(RecordFilterSchema, {
|
|
178
|
+
conditions: conditions.map(buildCondition),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildCondition(arg: ConditionArg): RecordCondition {
|
|
183
|
+
return create(RecordConditionSchema, {
|
|
184
|
+
field: arg.field,
|
|
185
|
+
op: CONDITION_OPS[arg.op] ?? RecordConditionOp.record_condition_op_unspecified,
|
|
186
|
+
value: arg.value !== undefined ? scalarValue(arg.value) : undefined,
|
|
187
|
+
values: (arg.values ?? []).map(scalarValue),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function buildOrderBy(
|
|
192
|
+
orderBy: { field: string; direction?: string } | undefined,
|
|
193
|
+
): RecordOrderBy | undefined {
|
|
194
|
+
if (orderBy === undefined) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
return create(RecordOrderBySchema, {
|
|
198
|
+
field: orderBy.field,
|
|
199
|
+
direction: orderBy.direction === "desc"
|
|
200
|
+
? RecordSortDirection.desc
|
|
201
|
+
: orderBy.direction === "asc"
|
|
202
|
+
? RecordSortDirection.asc
|
|
203
|
+
: RecordSortDirection.record_sort_direction_unspecified,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function scalarValue(v: string | number | boolean | null): Value {
|
|
208
|
+
return fromJson(ValueSchema, v);
|
|
209
|
+
}
|