@stigmer/mcp-server 3.3.0 → 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 +5 -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/tools.d.ts.map +1 -1
- package/domains/records/tools.js +7 -3
- package/domains/records/tools.js.map +1 -1
- 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/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/package.json +3 -3
- package/server.d.ts.map +1 -1
- package/server.js +12 -0
- package/server.js.map +1 -1
- 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/tools.ts +7 -3
- 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/datastore.ts +264 -0
- package/src/gen/environment.ts +66 -0
- package/src/server.ts +12 -0
|
@@ -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
|
});
|
|
@@ -84,8 +84,10 @@ export function registerRecordTools(
|
|
|
84
84
|
description:
|
|
85
85
|
"Describe a datastore: its collections, field declarations and encodings, constraint " +
|
|
86
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).
|
|
88
|
-
|
|
87
|
+
"(empty access means you are not allowed to touch that collection). A read verb may " +
|
|
88
|
+
"carry readable_fields: reads then return only those fields, and only they may appear " +
|
|
89
|
+
"in filter conditions and order_by (empty readable_fields means every field). Call " +
|
|
90
|
+
`this before the first record operation against an unfamiliar datastore. ${ENCODINGS}`,
|
|
89
91
|
inputSchema: {
|
|
90
92
|
datastore: z.string().describe("Datastore slug (e.g. clinic)."),
|
|
91
93
|
...orgShape,
|
|
@@ -104,7 +106,9 @@ export function registerRecordTools(
|
|
|
104
106
|
description:
|
|
105
107
|
"Find records in a datastore collection with a typed filter (conditions are AND-combined). " +
|
|
106
108
|
"Returns records plus total/limit/offset for paging. Results are ordered by created_at " +
|
|
107
|
-
|
|
109
|
+
"descending unless order_by is given. Records carry only the fields your grant allows " +
|
|
110
|
+
"(describe_datastore lists them as readable_fields); conditions and order_by on other " +
|
|
111
|
+
`fields are rejected. ${ENCODINGS}`,
|
|
108
112
|
inputSchema: {
|
|
109
113
|
datastore: z.string().describe("Datastore slug (e.g. clinic)."),
|
|
110
114
|
...orgShape,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// In-process integration test for the
|
|
1
|
+
// In-process integration test for the resource templates. Verifies template
|
|
2
2
|
// discovery, that each read returns the backend protojson as a single
|
|
3
3
|
// application/json entry, and that the skill templates resolve latest (empty
|
|
4
4
|
// version) vs a pinned version.
|
|
@@ -17,6 +17,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
17
17
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
18
18
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
19
19
|
import { AgentQueryController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/query_pb";
|
|
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";
|
|
20
24
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
21
25
|
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
22
26
|
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
@@ -42,6 +46,16 @@ const workflow = create(WorkflowSchema, {
|
|
|
42
46
|
kind: "workflow",
|
|
43
47
|
metadata: { slug: "w", org: "acme" },
|
|
44
48
|
});
|
|
49
|
+
const environment = create(EnvironmentSchema, {
|
|
50
|
+
apiVersion: "v1",
|
|
51
|
+
kind: "environment",
|
|
52
|
+
metadata: { slug: "e", org: "acme" },
|
|
53
|
+
});
|
|
54
|
+
const datastore = create(DatastoreSchema, {
|
|
55
|
+
apiVersion: "v1",
|
|
56
|
+
kind: "datastore",
|
|
57
|
+
metadata: { slug: "d", org: "acme" },
|
|
58
|
+
});
|
|
45
59
|
|
|
46
60
|
let backend: Http2Server;
|
|
47
61
|
let client: Client;
|
|
@@ -63,6 +77,8 @@ beforeAll(async () => {
|
|
|
63
77
|
},
|
|
64
78
|
});
|
|
65
79
|
router.service(WorkflowQueryController, { getByReference: () => workflow });
|
|
80
|
+
router.service(EnvironmentQueryController, { getByReference: () => environment });
|
|
81
|
+
router.service(DatastoreQueryController, { getByReference: () => datastore });
|
|
66
82
|
};
|
|
67
83
|
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
68
84
|
backend.on("session", (session) => {
|
|
@@ -85,7 +101,7 @@ afterAll(async () => {
|
|
|
85
101
|
});
|
|
86
102
|
|
|
87
103
|
describe("resource templates integration", () => {
|
|
88
|
-
it("advertises
|
|
104
|
+
it("advertises every template", async () => {
|
|
89
105
|
const { resourceTemplates } = await client.listResourceTemplates();
|
|
90
106
|
expect(resourceTemplates.map((t) => t.name)).toEqual(
|
|
91
107
|
expect.arrayContaining([
|
|
@@ -94,6 +110,8 @@ describe("resource templates integration", () => {
|
|
|
94
110
|
"stigmer_skill",
|
|
95
111
|
"stigmer_skill_version",
|
|
96
112
|
"stigmer_workflow",
|
|
113
|
+
"stigmer_environment",
|
|
114
|
+
"stigmer_datastore",
|
|
97
115
|
]),
|
|
98
116
|
);
|
|
99
117
|
});
|
|
@@ -136,4 +154,22 @@ describe("resource templates integration", () => {
|
|
|
136
154
|
await client.readResource({ uri: "stigmer://skills/acme/s/v2.0.0" });
|
|
137
155
|
expect(lastSkillVersion).toBe("v2.0.0");
|
|
138
156
|
});
|
|
157
|
+
|
|
158
|
+
it("reads an environment resource", async () => {
|
|
159
|
+
const result = (await client.readResource({
|
|
160
|
+
uri: "stigmer://environments/acme/e",
|
|
161
|
+
})) as ResourceResult;
|
|
162
|
+
expect(JSON.parse(result.contents[0]?.text ?? "{}")).toEqual(
|
|
163
|
+
toJson(EnvironmentSchema, environment, { useProtoFieldName: true }),
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("reads a datastore resource", async () => {
|
|
168
|
+
const result = (await client.readResource({
|
|
169
|
+
uri: "stigmer://datastores/acme/d",
|
|
170
|
+
})) as ResourceResult;
|
|
171
|
+
expect(JSON.parse(result.contents[0]?.text ?? "{}")).toEqual(
|
|
172
|
+
toJson(DatastoreSchema, datastore, { useProtoFieldName: true }),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
139
175
|
});
|
|
@@ -75,6 +75,8 @@ describe("buildResourceURI", () => {
|
|
|
75
75
|
expect(buildResourceURI("mcp_server", "acme", "m")).toBe("stigmer://mcp-servers/acme/m");
|
|
76
76
|
expect(buildResourceURI("skill", "acme", "s")).toBe("stigmer://skills/acme/s");
|
|
77
77
|
expect(buildResourceURI("workflow", "acme", "w")).toBe("stigmer://workflows/acme/w");
|
|
78
|
+
expect(buildResourceURI("environment", "acme", "e")).toBe("stigmer://environments/acme/e");
|
|
79
|
+
expect(buildResourceURI("datastore", "acme", "d")).toBe("stigmer://datastores/acme/d");
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
it("round-trips with parseResourceURI", () => {
|
|
@@ -91,7 +93,14 @@ describe("buildResourceURI", () => {
|
|
|
91
93
|
expect(buildResourceURI("agent", "acme", "")).toBe("");
|
|
92
94
|
});
|
|
93
95
|
|
|
94
|
-
it("covers exactly the
|
|
95
|
-
expect(Object.keys(kindToAuthority).sort()).toEqual([
|
|
96
|
+
it("covers exactly the templated kinds", () => {
|
|
97
|
+
expect(Object.keys(kindToAuthority).sort()).toEqual([
|
|
98
|
+
"agent",
|
|
99
|
+
"datastore",
|
|
100
|
+
"environment",
|
|
101
|
+
"mcp_server",
|
|
102
|
+
"skill",
|
|
103
|
+
"workflow",
|
|
104
|
+
]);
|
|
96
105
|
});
|
|
97
106
|
});
|
|
@@ -81,6 +81,8 @@ export function parseVersionedResourceURI(uri: string): VersionedResourceRef {
|
|
|
81
81
|
*/
|
|
82
82
|
export const kindToAuthority: Readonly<Record<string, string>> = {
|
|
83
83
|
agent: "agents",
|
|
84
|
+
datastore: "datastores",
|
|
85
|
+
environment: "environments",
|
|
84
86
|
mcp_server: "mcp-servers",
|
|
85
87
|
skill: "skills",
|
|
86
88
|
workflow: "workflows",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// In-process integration test for the search tool. Verifies resource_uri
|
|
2
|
-
// enrichment for
|
|
2
|
+
// enrichment for every searchable kind, the empty-response short-circuit,
|
|
3
3
|
// the unknown-kind validation error, and pagination passthrough.
|
|
4
4
|
|
|
5
5
|
import { create, toJson } from "@bufbuild/protobuf";
|
|
@@ -35,6 +35,12 @@ const allKindsResponse = create(SearchResponseSchema, {
|
|
|
35
35
|
create(SearchResultSchema, { kind: ApiResourceKind.skill, org: "acme", slug: "code-review" }),
|
|
36
36
|
create(SearchResultSchema, { kind: ApiResourceKind.mcp_server, org: "acme", slug: "github" }),
|
|
37
37
|
create(SearchResultSchema, { kind: ApiResourceKind.workflow, org: "acme", slug: "release" }),
|
|
38
|
+
create(SearchResultSchema, {
|
|
39
|
+
kind: ApiResourceKind.environment,
|
|
40
|
+
org: "acme",
|
|
41
|
+
slug: "github-creds",
|
|
42
|
+
}),
|
|
43
|
+
create(SearchResultSchema, { kind: ApiResourceKind.datastore, org: "acme", slug: "bookings" }),
|
|
38
44
|
],
|
|
39
45
|
});
|
|
40
46
|
const emptyResponse = create(SearchResponseSchema, { entries: [] });
|
|
@@ -96,6 +102,8 @@ describe("search tool integration", () => {
|
|
|
96
102
|
"stigmer://skills/acme/code-review",
|
|
97
103
|
"stigmer://mcp-servers/acme/github",
|
|
98
104
|
"stigmer://workflows/acme/release",
|
|
105
|
+
"stigmer://environments/acme/github-creds",
|
|
106
|
+
"stigmer://datastores/acme/bookings",
|
|
99
107
|
]);
|
|
100
108
|
});
|
|
101
109
|
|
|
@@ -112,7 +120,15 @@ describe("search tool integration", () => {
|
|
|
112
120
|
const result = await callSearch({ kinds: ["bogus"] });
|
|
113
121
|
expect(result.isError).toBe(true);
|
|
114
122
|
expect(result.content[0]?.text).toContain('unknown resource kind "bogus"');
|
|
115
|
-
expect(result.content[0]?.text).toContain(
|
|
123
|
+
expect(result.content[0]?.text).toContain(
|
|
124
|
+
"valid kinds: agent, skill, mcp_server, workflow, environment, datastore",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("accepts the environment and datastore kinds", async () => {
|
|
129
|
+
nextResponse = emptyResponse;
|
|
130
|
+
const result = await callSearch({ kinds: ["environment", "datastore"] });
|
|
131
|
+
expect(result.isError).toBeFalsy();
|
|
116
132
|
});
|
|
117
133
|
|
|
118
134
|
it("forwards pagination only when requested", async () => {
|