@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.
Files changed (149) hide show
  1. package/README.md +60 -13
  2. package/cli/mcp-server-stigmer.js +5 -1
  3. package/domains/agentexecutions/approve.d.ts +9 -0
  4. package/domains/agentexecutions/approve.d.ts.map +1 -0
  5. package/domains/agentexecutions/approve.js +46 -0
  6. package/domains/agentexecutions/approve.js.map +1 -0
  7. package/domains/agentexecutions/fetch.d.ts +28 -0
  8. package/domains/agentexecutions/fetch.d.ts.map +1 -0
  9. package/domains/agentexecutions/fetch.js +82 -0
  10. package/domains/agentexecutions/fetch.js.map +1 -0
  11. package/domains/agentexecutions/run.d.ts +27 -0
  12. package/domains/agentexecutions/run.d.ts.map +1 -0
  13. package/domains/agentexecutions/run.js +87 -0
  14. package/domains/agentexecutions/run.js.map +1 -0
  15. package/domains/agentexecutions/tools.d.ts +5 -0
  16. package/domains/agentexecutions/tools.d.ts.map +1 -0
  17. package/domains/agentexecutions/tools.js +97 -0
  18. package/domains/agentexecutions/tools.js.map +1 -0
  19. package/domains/datastores/apply.d.ts +4 -0
  20. package/domains/datastores/apply.d.ts.map +1 -0
  21. package/domains/datastores/apply.js +28 -0
  22. package/domains/datastores/apply.js.map +1 -0
  23. package/domains/datastores/delete.d.ts +3 -0
  24. package/domains/datastores/delete.d.ts.map +1 -0
  25. package/domains/datastores/delete.js +38 -0
  26. package/domains/datastores/delete.js.map +1 -0
  27. package/domains/datastores/fetch.d.ts +3 -0
  28. package/domains/datastores/fetch.d.ts.map +1 -0
  29. package/domains/datastores/fetch.js +23 -0
  30. package/domains/datastores/fetch.js.map +1 -0
  31. package/domains/datastores/resources.d.ts +5 -0
  32. package/domains/datastores/resources.d.ts.map +1 -0
  33. package/domains/datastores/resources.js +16 -0
  34. package/domains/datastores/resources.js.map +1 -0
  35. package/domains/datastores/tools.d.ts +5 -0
  36. package/domains/datastores/tools.d.ts.map +1 -0
  37. package/domains/datastores/tools.js +46 -0
  38. package/domains/datastores/tools.js.map +1 -0
  39. package/domains/environments/apply.d.ts +4 -0
  40. package/domains/environments/apply.d.ts.map +1 -0
  41. package/domains/environments/apply.js +30 -0
  42. package/domains/environments/apply.js.map +1 -0
  43. package/domains/environments/delete.d.ts +3 -0
  44. package/domains/environments/delete.d.ts.map +1 -0
  45. package/domains/environments/delete.js +36 -0
  46. package/domains/environments/delete.js.map +1 -0
  47. package/domains/environments/fetch.d.ts +6 -0
  48. package/domains/environments/fetch.d.ts.map +1 -0
  49. package/domains/environments/fetch.js +30 -0
  50. package/domains/environments/fetch.js.map +1 -0
  51. package/domains/environments/resources.d.ts +5 -0
  52. package/domains/environments/resources.d.ts.map +1 -0
  53. package/domains/environments/resources.js +16 -0
  54. package/domains/environments/resources.js.map +1 -0
  55. package/domains/environments/tools.d.ts +5 -0
  56. package/domains/environments/tools.d.ts.map +1 -0
  57. package/domains/environments/tools.js +51 -0
  58. package/domains/environments/tools.js.map +1 -0
  59. package/domains/executions/cancel.d.ts +9 -0
  60. package/domains/executions/cancel.d.ts.map +1 -0
  61. package/domains/executions/cancel.js +114 -0
  62. package/domains/executions/cancel.js.map +1 -0
  63. package/domains/executions/tools.d.ts +5 -0
  64. package/domains/executions/tools.d.ts.map +1 -0
  65. package/domains/executions/tools.js +28 -0
  66. package/domains/executions/tools.js.map +1 -0
  67. package/domains/records/tools.d.ts.map +1 -1
  68. package/domains/records/tools.js +7 -3
  69. package/domains/records/tools.js.map +1 -1
  70. package/domains/resourceuri.d.ts.map +1 -1
  71. package/domains/resourceuri.js +2 -0
  72. package/domains/resourceuri.js.map +1 -1
  73. package/domains/search/tools.d.ts.map +1 -1
  74. package/domains/search/tools.js +10 -7
  75. package/domains/search/tools.js.map +1 -1
  76. package/domains/skills/tools.d.ts.map +1 -1
  77. package/domains/skills/tools.js +25 -1
  78. package/domains/skills/tools.js.map +1 -1
  79. package/domains/skills/versions.d.ts +9 -0
  80. package/domains/skills/versions.d.ts.map +1 -0
  81. package/domains/skills/versions.js +31 -0
  82. package/domains/skills/versions.js.map +1 -0
  83. package/domains/workflowexecutions/approvals.d.ts +17 -0
  84. package/domains/workflowexecutions/approvals.d.ts.map +1 -0
  85. package/domains/workflowexecutions/approvals.js +61 -0
  86. package/domains/workflowexecutions/approvals.js.map +1 -0
  87. package/domains/workflowexecutions/run.d.ts +12 -0
  88. package/domains/workflowexecutions/run.d.ts.map +1 -0
  89. package/domains/workflowexecutions/run.js +69 -0
  90. package/domains/workflowexecutions/run.js.map +1 -0
  91. package/domains/workflowexecutions/tools.d.ts.map +1 -1
  92. package/domains/workflowexecutions/tools.js +89 -6
  93. package/domains/workflowexecutions/tools.js.map +1 -1
  94. package/domains/workflows/tools.d.ts.map +1 -1
  95. package/domains/workflows/tools.js +69 -5
  96. package/domains/workflows/tools.js.map +1 -1
  97. package/domains/workflows/versions.d.ts +29 -0
  98. package/domains/workflows/versions.d.ts.map +1 -0
  99. package/domains/workflows/versions.js +99 -0
  100. package/domains/workflows/versions.js.map +1 -0
  101. package/gen/datastore.d.ts +861 -0
  102. package/gen/datastore.d.ts.map +1 -0
  103. package/gen/datastore.js +267 -0
  104. package/gen/datastore.js.map +1 -0
  105. package/gen/environment.d.ts +77 -0
  106. package/gen/environment.d.ts.map +1 -0
  107. package/gen/environment.js +62 -0
  108. package/gen/environment.js.map +1 -0
  109. package/package.json +3 -3
  110. package/server.d.ts.map +1 -1
  111. package/server.js +12 -0
  112. package/server.js.map +1 -1
  113. package/src/domains/agentexecutions/approve.ts +67 -0
  114. package/src/domains/agentexecutions/fetch.ts +112 -0
  115. package/src/domains/agentexecutions/run.ts +112 -0
  116. package/src/domains/agentexecutions/tools.ts +139 -0
  117. package/src/domains/apply.integration.test.ts +82 -6
  118. package/src/domains/datastores/apply.ts +33 -0
  119. package/src/domains/datastores/delete.ts +47 -0
  120. package/src/domains/datastores/fetch.ts +32 -0
  121. package/src/domains/datastores/resources.ts +21 -0
  122. package/src/domains/datastores/tools.ts +76 -0
  123. package/src/domains/deletes.integration.test.ts +49 -2
  124. package/src/domains/environments/apply.ts +40 -0
  125. package/src/domains/environments/delete.ts +45 -0
  126. package/src/domains/environments/fetch.ts +39 -0
  127. package/src/domains/environments/resources.ts +21 -0
  128. package/src/domains/environments/tools.ts +81 -0
  129. package/src/domains/executions/cancel.ts +148 -0
  130. package/src/domains/executions/tools.ts +45 -0
  131. package/src/domains/executions.integration.test.ts +376 -0
  132. package/src/domains/reads.integration.test.ts +51 -5
  133. package/src/domains/records/tools.ts +7 -3
  134. package/src/domains/resources.integration.test.ts +38 -2
  135. package/src/domains/resourceuri.test.ts +11 -2
  136. package/src/domains/resourceuri.ts +2 -0
  137. package/src/domains/search/search.integration.test.ts +18 -2
  138. package/src/domains/search/tools.ts +12 -7
  139. package/src/domains/skills/tools.ts +34 -1
  140. package/src/domains/skills/versions.ts +47 -0
  141. package/src/domains/versions.integration.test.ts +212 -0
  142. package/src/domains/workflowexecutions/approvals.ts +102 -0
  143. package/src/domains/workflowexecutions/run.ts +87 -0
  144. package/src/domains/workflowexecutions/tools.ts +120 -6
  145. package/src/domains/workflows/tools.ts +90 -7
  146. package/src/domains/workflows/versions.ts +147 -0
  147. package/src/gen/datastore.ts +264 -0
  148. package/src/gen/environment.ts +66 -0
  149. package/src/server.ts +12 -0
@@ -0,0 +1,67 @@
1
+ // Agent-execution approval path: submit a decision for a tool call the
2
+ // execution is waiting on (AgentExecutionCommandController.submitApproval).
3
+ //
4
+ // Pending approvals surface in get_agent_execution's
5
+ // status.pending_approvals[] — there is no org-wide inbox for agent
6
+ // executions (that exists only for workflow human_input tasks, see
7
+ // workflowexecutions/approvals.ts). The response reuses the compact
8
+ // projection: the returned AgentExecution embeds the full message history,
9
+ // which the approval loop doesn't need.
10
+
11
+ import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
12
+ import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
13
+
14
+ import { withClient } from "../client.js";
15
+ import { rpcError } from "../rpcerr.js";
16
+ import { compactExecutionJson, DEFAULT_MESSAGE_LIMIT } from "./fetch.js";
17
+
18
+ /**
19
+ * Model-facing action spelling → proto enum. APPROVE_ALL is deliberately not
20
+ * exposed: blanket auto-approval is a run-configuration concern, not a
21
+ * per-decision one.
22
+ */
23
+ const APPROVAL_ACTIONS: Readonly<Record<string, ApprovalAction>> = {
24
+ approve: ApprovalAction.APPROVE,
25
+ skip: ApprovalAction.SKIP,
26
+ reject: ApprovalAction.REJECT,
27
+ };
28
+
29
+ export interface SubmitAgentApprovalArgs {
30
+ readonly executionId: string;
31
+ readonly toolCallId: string;
32
+ readonly action: string;
33
+ readonly comment?: string;
34
+ }
35
+
36
+ /** Submit an approval decision; returns the execution in the compact view. */
37
+ export async function submitAgentApproval(
38
+ serverAddress: string,
39
+ token: string,
40
+ args: SubmitAgentApprovalArgs,
41
+ ): Promise<string> {
42
+ const action = APPROVAL_ACTIONS[args.action];
43
+ if (action === undefined) {
44
+ throw new Error(`unknown action "${args.action}"; valid actions: approve, skip, reject`);
45
+ }
46
+ return withClient(
47
+ AgentExecutionCommandController,
48
+ serverAddress,
49
+ token,
50
+ async (client, callOptions) => {
51
+ try {
52
+ const execution = await client.submitApproval(
53
+ {
54
+ agentExecutionId: args.executionId,
55
+ toolCallId: args.toolCallId,
56
+ action,
57
+ comment: args.comment ?? "",
58
+ },
59
+ callOptions,
60
+ );
61
+ return compactExecutionJson(execution, DEFAULT_MESSAGE_LIMIT);
62
+ } catch (err) {
63
+ throw rpcError(err, `approval for agent execution "${args.executionId}"`);
64
+ }
65
+ },
66
+ );
67
+ }
@@ -0,0 +1,112 @@
1
+ // Agent-execution read path: the polling primitive behind get_agent_execution.
2
+ //
3
+ // Agent executions have no event-log RPC (unlike workflow executions) — the
4
+ // platform's contract is: poll get and read status.phase, status.messages[],
5
+ // and status.pending_approvals[]. That makes the response shape critical for
6
+ // MCP: a long conversation's full protojson (every message, the resolved
7
+ // context snapshot, the approval ledger, sub-agent transcripts) can dwarf the
8
+ // model's context. The default "compact" view therefore returns a bounded
9
+ // message tail and drops the bulky server-side bookkeeping fields; "full" is
10
+ // the verbatim protojson for when the model genuinely needs everything.
11
+
12
+ import { AgentExecutionSchema, type AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
13
+ import { AgentExecutionQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/query_pb";
14
+
15
+ import { withClient } from "../client.js";
16
+ import { toProtoJson } from "../marshal.js";
17
+ import { rpcError } from "../rpcerr.js";
18
+
19
+ export type ExecutionView = "compact" | "full";
20
+
21
+ /** Message-tail size when the caller doesn't specify one. */
22
+ export const DEFAULT_MESSAGE_LIMIT = 5;
23
+
24
+ /**
25
+ * Bulky status fields pruned from the compact view. These are server-side
26
+ * bookkeeping (resolved-resource snapshot, append-only approval ledger,
27
+ * Temporal callback token) and sub-agent transcripts — none of which the
28
+ * poll loop (phase? messages? approvals?) needs.
29
+ */
30
+ const COMPACT_PRUNED_STATUS_FIELDS = [
31
+ "resolved_context",
32
+ "approval_events",
33
+ "callback_token",
34
+ "sub_agent_executions",
35
+ ] as const;
36
+
37
+ /** Fetch a single agent execution by ID and shape it for the requested view. */
38
+ export async function fetchAgentExecution(
39
+ serverAddress: string,
40
+ token: string,
41
+ executionId: string,
42
+ view: ExecutionView,
43
+ messageLimit: number,
44
+ ): Promise<string> {
45
+ if (executionId === "") {
46
+ throw new Error("execution_id is required");
47
+ }
48
+ return withClient(
49
+ AgentExecutionQueryController,
50
+ serverAddress,
51
+ token,
52
+ async (client, callOptions) => {
53
+ let execution: AgentExecution;
54
+ try {
55
+ execution = await client.get({ value: executionId }, callOptions);
56
+ } catch (err) {
57
+ throw rpcError(err, `agent execution "${executionId}"`);
58
+ }
59
+ return view === "full"
60
+ ? toProtoJson(AgentExecutionSchema, execution)
61
+ : compactExecutionJson(execution, messageLimit);
62
+ },
63
+ );
64
+ }
65
+
66
+ /** The compact projection plus the pre-truncation message count. */
67
+ export interface CompactExecution {
68
+ readonly totalMessages: number;
69
+ readonly data: Record<string, unknown>;
70
+ }
71
+
72
+ /**
73
+ * Build the compact projection: full protojson minus the pruned status
74
+ * fields, with status.messages sliced to the last `messageLimit` entries.
75
+ * Exposed at the data level so wrappers (cancel_execution's already_terminal
76
+ * envelope) can compose it without double-nesting.
77
+ *
78
+ * Shared by the write tools that return an AgentExecution (approve, cancel):
79
+ * their responses embed the same potentially-huge status.
80
+ */
81
+ export function compactExecution(execution: AgentExecution, messageLimit: number): CompactExecution {
82
+ const data = JSON.parse(toProtoJson(AgentExecutionSchema, execution)) as Record<string, unknown>;
83
+ let totalMessages = 0;
84
+
85
+ const status = data.status as Record<string, unknown> | undefined;
86
+ if (status !== undefined) {
87
+ const messages: unknown[] = Array.isArray(status.messages) ? status.messages : [];
88
+ totalMessages = messages.length;
89
+ if (messages.length > messageLimit) {
90
+ status.messages = messages.slice(-messageLimit);
91
+ }
92
+ for (const field of COMPACT_PRUNED_STATUS_FIELDS) {
93
+ delete status[field];
94
+ }
95
+ }
96
+
97
+ return { totalMessages, data };
98
+ }
99
+
100
+ /**
101
+ * The compact view as returned by tools: a wrapper carrying total_messages so
102
+ * the model can tell when the message tail is a window (and re-request with a
103
+ * larger message_limit or view=full).
104
+ */
105
+ export function compactExecutionJson(execution: AgentExecution, messageLimit: number): string {
106
+ const { totalMessages, data } = compactExecution(execution, messageLimit);
107
+ return JSON.stringify(
108
+ { view: "compact", total_messages: totalMessages, execution: data },
109
+ null,
110
+ 2,
111
+ );
112
+ }
@@ -0,0 +1,112 @@
1
+ // Agent-execution start path for the run_agent tool.
2
+ //
3
+ // Mirrors the CLI's run stack (client-apps/cli/src/resources/run/create.ts):
4
+ // starting an agent is a single AgentExecutionCommandController.create call —
5
+ // the server bootstraps the session, resolves the agent's default instance
6
+ // (auto-creating if missing), and dispatches the message. The MCP layer only
7
+ // resolves the org/slug reference to the agent ID first, over the same
8
+ // transport (the two-step pattern the delete tools use).
9
+ //
10
+ // The tool is deliberately asynchronous: it returns the created execution
11
+ // (with its aex_* ID) immediately and the run continues in the background.
12
+ // Observation happens through get_agent_execution polling — MCP tools are
13
+ // request/response, so there is no streaming path here by design.
14
+
15
+ import { createClient } from "@connectrpc/connect";
16
+ import { create as createMessage } from "@bufbuild/protobuf";
17
+ import { AgentQueryController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/query_pb";
18
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
19
+ import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
20
+ import { AgentExecutionSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
21
+ import {
22
+ ExecutionValueSchema,
23
+ type ExecutionValue,
24
+ } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/spec_pb";
25
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
26
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
27
+
28
+ import { withTransport } from "../client.js";
29
+ import { toProtoJson } from "../marshal.js";
30
+ import { rpcError } from "../rpcerr.js";
31
+
32
+ /** apiVersion stamped on created executions; mirrors the CLI's run stack. */
33
+ const API_VERSION = "agentic.stigmer.ai/v1";
34
+
35
+ export interface RunAgentArgs {
36
+ readonly org: string;
37
+ readonly agent: string;
38
+ readonly message: string;
39
+ readonly sessionId?: string;
40
+ readonly runtimeEnv?: Record<string, string>;
41
+ }
42
+
43
+ /**
44
+ * Start an agent execution: resolve org/slug → agent ID, then create the
45
+ * execution. Returns the created execution as protojson (small at creation
46
+ * time — status is empty until the runner picks it up).
47
+ */
48
+ export async function runAgent(
49
+ serverAddress: string,
50
+ token: string,
51
+ args: RunAgentArgs,
52
+ ): Promise<string> {
53
+ const desc = `agent "${args.agent}" in org "${args.org}"`;
54
+ return withTransport(serverAddress, token, async (transport, callOptions) => {
55
+ const query = createClient(AgentQueryController, transport);
56
+ let agentId: string;
57
+ try {
58
+ const agent = await query.getByReference(
59
+ { org: args.org, kind: ApiResourceKind.agent, slug: args.agent },
60
+ callOptions,
61
+ );
62
+ agentId = agent.metadata?.id ?? "";
63
+ } catch (err) {
64
+ throw rpcError(err, desc);
65
+ }
66
+
67
+ const execution = createMessage(AgentExecutionSchema, {
68
+ apiVersion: API_VERSION,
69
+ kind: "AgentExecution",
70
+ metadata: createMessage(ApiResourceMetadataSchema, { name: executionName(), org: args.org }),
71
+ spec: createMessage(AgentExecutionSpecSchema, {
72
+ // Empty message means "just run" — the CLI applies the same default.
73
+ message: args.message === "" ? "execute" : args.message,
74
+ runtimeEnv: toExecutionValues(args.runtimeEnv),
75
+ sessionId: args.sessionId ?? "",
76
+ agentId,
77
+ }),
78
+ });
79
+
80
+ const command = createClient(AgentExecutionCommandController, transport);
81
+ try {
82
+ const created = await command.create(execution, callOptions);
83
+ return toProtoJson(AgentExecutionSchema, created);
84
+ } catch (err) {
85
+ throw rpcError(err, `execution of ${desc}`);
86
+ }
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Convert the tool's plain string map to the proto ExecutionValue map. Values
92
+ * arriving through an MCP tool call have already passed through the model's
93
+ * context, so they are never secrets by definition — secrets reach executions
94
+ * through Environments, not through this tool.
95
+ */
96
+ export function toExecutionValues(
97
+ env: Record<string, string> | undefined,
98
+ ): Record<string, ExecutionValue> {
99
+ const out: Record<string, ExecutionValue> = {};
100
+ for (const [key, value] of Object.entries(env ?? {})) {
101
+ out[key] = createMessage(ExecutionValueSchema, { value, isSecret: false });
102
+ }
103
+ return out;
104
+ }
105
+
106
+ /**
107
+ * Unique-enough placeholder name; the backend owns final identity. Mirrors the
108
+ * CLI's executionName (Go's fmt.Sprintf("execution-%d", UnixMicro())).
109
+ */
110
+ export function executionName(): string {
111
+ return `execution-${Date.now() * 1000}`;
112
+ }
@@ -0,0 +1,139 @@
1
+ // MCP tools for the AgentExecution domain: start a run, poll it, and answer
2
+ // its approval requests. Together with cancel_execution (executions domain)
3
+ // these close the agent iteration loop: author with apply_agent, run with
4
+ // run_agent, observe with get_agent_execution, steer with
5
+ // submit_agent_execution_approval.
6
+ //
7
+ // The execution model is asynchronous by design: run_agent returns as soon as
8
+ // the backend accepts the execution; progress is observed by polling. Agent
9
+ // executions have no event-log RPC, so get_agent_execution IS the poll loop —
10
+ // which is why it defaults to the compact view (see fetch.ts).
11
+
12
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { z } from "zod";
14
+
15
+ import { resolveToken, type BackendTarget } from "../client.js";
16
+ import { textOrError } from "../toolresult.js";
17
+ import { submitAgentApproval } from "./approve.js";
18
+ import { DEFAULT_MESSAGE_LIMIT, fetchAgentExecution } from "./fetch.js";
19
+ import { runAgent } from "./run.js";
20
+
21
+ /** Register every AgentExecution-domain tool; returns the registered tool names. */
22
+ export function registerAgentExecutionTools(server: McpServer, target: BackendTarget): string[] {
23
+ server.registerTool(
24
+ "run_agent",
25
+ {
26
+ description:
27
+ "Start an agent execution (asynchronous). Returns immediately with the created execution " +
28
+ "(aex_* ID) while the run continues in the background — poll get_agent_execution to observe " +
29
+ "progress, pending approvals, and the final result. Omit session_id to start a fresh " +
30
+ "conversation; pass one to send a follow-up message into an existing session.",
31
+ inputSchema: {
32
+ org: z.string().describe("Organization slug that owns the agent (e.g. stigmer)."),
33
+ agent: z
34
+ .string()
35
+ .describe("Agent slug — the unique identifier within the org (e.g. code-reviewer)."),
36
+ message: z.string().describe("The instruction or message for the agent to act on."),
37
+ session_id: z
38
+ .string()
39
+ .optional()
40
+ .describe(
41
+ "Existing session ID to continue a conversation (from a previous execution's " +
42
+ "spec.session_id). Omit to start a new session.",
43
+ ),
44
+ runtime_env: z
45
+ .record(z.string())
46
+ .optional()
47
+ .describe(
48
+ "Non-secret runtime environment values (name → value) injected into the run. " +
49
+ "Secrets must come from Environments attached to the agent, never through this tool.",
50
+ ),
51
+ },
52
+ },
53
+ (args, extra) =>
54
+ textOrError(() =>
55
+ runAgent(target.serverAddress, resolveToken(extra, target.apiKey), {
56
+ org: args.org,
57
+ agent: args.agent,
58
+ message: args.message,
59
+ sessionId: args.session_id,
60
+ runtimeEnv: args.runtime_env,
61
+ }),
62
+ ),
63
+ );
64
+
65
+ server.registerTool(
66
+ "get_agent_execution",
67
+ {
68
+ description:
69
+ "Get an agent execution's status: phase, messages, pending approvals, errors, timing. " +
70
+ "Agent executions have no event log — poll this tool to track a run started with run_agent " +
71
+ "(terminal phases: completed, failed, cancelled, terminated). The default compact view " +
72
+ "returns the last few messages and omits bulky bookkeeping fields (resolved context, " +
73
+ "approval ledger, sub-agent transcripts); total_messages tells you when the tail is a " +
74
+ "window. Use view=full for the complete record.",
75
+ inputSchema: {
76
+ execution_id: z.string().describe("Agent execution ID (aex_* format)."),
77
+ view: z
78
+ .enum(["compact", "full"])
79
+ .optional()
80
+ .describe("Response shape: compact (default, bounded message tail) or full protojson."),
81
+ message_limit: z
82
+ .number()
83
+ .int()
84
+ .min(1)
85
+ .optional()
86
+ .describe(`Compact view only: number of trailing messages to return (default ${DEFAULT_MESSAGE_LIMIT}).`),
87
+ },
88
+ },
89
+ (args, extra) =>
90
+ textOrError(() =>
91
+ fetchAgentExecution(
92
+ target.serverAddress,
93
+ resolveToken(extra, target.apiKey),
94
+ args.execution_id,
95
+ args.view ?? "compact",
96
+ args.message_limit ?? DEFAULT_MESSAGE_LIMIT,
97
+ ),
98
+ ),
99
+ );
100
+
101
+ server.registerTool(
102
+ "submit_agent_execution_approval",
103
+ {
104
+ description:
105
+ "Approve, skip, or reject a tool call an agent execution is waiting on (phase " +
106
+ "waiting-for-approval). Find pending requests in get_agent_execution's " +
107
+ "status.pending_approvals — tool_call_id must match exactly. reject denies the single " +
108
+ "tool call and feeds your comment back to the agent, which then continues; to stop the " +
109
+ "whole run use cancel_execution instead.",
110
+ inputSchema: {
111
+ execution_id: z.string().describe("Agent execution ID (aex_* format)."),
112
+ tool_call_id: z
113
+ .string()
114
+ .describe("Tool call awaiting the decision (status.pending_approvals[].tool_call_id)."),
115
+ action: z
116
+ .enum(["approve", "skip", "reject"])
117
+ .describe(
118
+ "approve: execute the tool. skip: don't execute, agent proceeds without it. " +
119
+ "reject: don't execute, agent is told why (see comment) and adapts.",
120
+ ),
121
+ comment: z
122
+ .string()
123
+ .optional()
124
+ .describe("Reason for the decision; on reject it is fed back to the agent."),
125
+ },
126
+ },
127
+ (args, extra) =>
128
+ textOrError(() =>
129
+ submitAgentApproval(target.serverAddress, resolveToken(extra, target.apiKey), {
130
+ executionId: args.execution_id,
131
+ toolCallId: args.tool_call_id,
132
+ action: args.action,
133
+ comment: args.comment,
134
+ }),
135
+ ),
136
+ );
137
+
138
+ return ["run_agent", "get_agent_execution", "submit_agent_execution_approval"];
139
+ }
@@ -1,9 +1,11 @@
1
1
  // In-process integration test for the apply tools. Drives apply_agent,
2
- // apply_mcp_server, and apply_workflow through the full MCP boundary and asserts
3
- // the codegen projection (src/gen/*) reconstitutes the proto correctly: metadata
4
- // hoist + slug generation, enum-string conversion, ApiResourceReference kind
5
- // injection, the stdio/http oneof, and the recursive workflow task_config
6
- // expansion (http_call leaf, fork/for_each nesting).
2
+ // apply_mcp_server, apply_workflow, apply_environment, and apply_datastore
3
+ // through the full MCP boundary and asserts the codegen projection (src/gen/*)
4
+ // reconstitutes the proto correctly: metadata hoist + slug generation,
5
+ // enum-string conversion (including repeated enums — datastore grant verbs),
6
+ // ApiResourceReference kind injection, the stdio/http oneof, the environment
7
+ // data map with secret flags, and the recursive workflow task_config expansion
8
+ // (http_call leaf, fork/for_each nesting).
7
9
 
8
10
  import type { ConnectRouter } from "@connectrpc/connect";
9
11
  import { connectNodeAdapter } from "@connectrpc/connect-node";
@@ -18,6 +20,11 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
18
20
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
19
21
  import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
20
22
  import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
23
+ import type { Datastore } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
24
+ import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
25
+ import { DatastoreVerb, FieldType } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/spec_pb";
26
+ import type { Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
27
+ import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
21
28
  import type { McpServer as McpServerProto } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
22
29
  import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
23
30
  import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
@@ -37,6 +44,8 @@ let client: Client;
37
44
  let appliedAgent: Agent | undefined;
38
45
  let appliedMcpServer: McpServerProto | undefined;
39
46
  let appliedWorkflow: Workflow | undefined;
47
+ let appliedEnvironment: Environment | undefined;
48
+ let appliedDatastore: Datastore | undefined;
40
49
  const openSessions = new Set<ServerHttp2Session>();
41
50
 
42
51
  interface ToolResult {
@@ -68,6 +77,18 @@ beforeAll(async () => {
68
77
  return req;
69
78
  },
70
79
  });
80
+ router.service(EnvironmentCommandController, {
81
+ apply: (req) => {
82
+ appliedEnvironment = req;
83
+ return req;
84
+ },
85
+ });
86
+ router.service(DatastoreCommandController, {
87
+ apply: (req) => {
88
+ appliedDatastore = req;
89
+ return req;
90
+ },
91
+ });
71
92
  };
72
93
  backend = createHttp2Server(connectNodeAdapter({ routes }));
73
94
  backend.on("session", (session) => {
@@ -93,7 +114,13 @@ describe("apply tools integration", () => {
93
114
  it("advertises every apply tool", async () => {
94
115
  const { tools } = await client.listTools();
95
116
  expect(tools.map((t) => t.name)).toEqual(
96
- expect.arrayContaining(["apply_agent", "apply_mcp_server", "apply_workflow"]),
117
+ expect.arrayContaining([
118
+ "apply_agent",
119
+ "apply_mcp_server",
120
+ "apply_workflow",
121
+ "apply_environment",
122
+ "apply_datastore",
123
+ ]),
97
124
  );
98
125
  });
99
126
 
@@ -217,4 +244,53 @@ describe("apply tools integration", () => {
217
244
  expect(forCfg.each).toBe("item");
218
245
  expect(forCfg.do).toHaveLength(1);
219
246
  });
247
+
248
+ it("apply_environment rebuilds the data map with secret flags", async () => {
249
+ const result = await callTool("apply_environment", {
250
+ name: "GitHub Creds",
251
+ org: "acme",
252
+ data: {
253
+ API_KEY: { value: "sk-real-value", is_secret: true, description: "GitHub PAT" },
254
+ REGION: { value: "us-east-1" },
255
+ },
256
+ });
257
+ expect(result.isError).toBeFalsy();
258
+
259
+ const env = appliedEnvironment;
260
+ expect(env?.apiVersion).toBe("agentic.stigmer.ai/v1");
261
+ expect(env?.kind).toBe("Environment");
262
+ expect(env?.metadata?.slug).toBe("github-creds"); // auto-generated from name
263
+ expect(env?.spec?.data?.API_KEY).toMatchObject({
264
+ value: "sk-real-value",
265
+ isSecret: true,
266
+ description: "GitHub PAT",
267
+ });
268
+ expect(env?.spec?.data?.REGION?.isSecret).toBe(false);
269
+ });
270
+
271
+ it("apply_datastore maps field types and repeated grant verbs to enums", async () => {
272
+ const result = await callTool("apply_datastore", {
273
+ name: "Bookings",
274
+ org: "acme",
275
+ collections: [
276
+ {
277
+ name: "appointments",
278
+ fields: [{ name: "patient", type: "string", required: true }],
279
+ grants: [{ role: "assistant", verbs: ["read", "insert"], scope: "own" }],
280
+ },
281
+ ],
282
+ });
283
+ expect(result.isError).toBeFalsy();
284
+
285
+ const ds = appliedDatastore;
286
+ expect(ds?.kind).toBe("Datastore");
287
+ expect(ds?.metadata?.slug).toBe("bookings");
288
+
289
+ const collection = ds?.spec?.collections?.[0];
290
+ expect(collection?.fields?.[0]?.type).toBe(FieldType.string);
291
+
292
+ // Repeated enum: verb strings map to DatastoreVerb values, not raw strings.
293
+ const grant = collection?.grants?.[0];
294
+ expect(grant?.verbs).toEqual([DatastoreVerb.read, DatastoreVerb.insert]);
295
+ });
220
296
  });
@@ -0,0 +1,33 @@
1
+ // Datastore apply path: create-or-update via DatastoreCommandController.apply.
2
+ // The flat MCP input is projected into a fully-formed Datastore proto by the
3
+ // generated datastoreInputToProto bridge (codegen, src/gen/datastore.ts).
4
+ //
5
+ // The manifest is authoritative for structure, never for data: schema changes
6
+ // sync on apply (the server enforces its additive-plus change matrix), and
7
+ // records enter exclusively through the record tools.
8
+
9
+ import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
10
+ import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
11
+
12
+ import { datastoreInputToProto, type DatastoreInput } from "../../gen/datastore.js";
13
+ import { withClient } from "../client.js";
14
+ import { toProtoJson } from "../marshal.js";
15
+ import { rpcError } from "../rpcerr.js";
16
+
17
+ /** Create or update a datastore, returning the persisted resource as protojson. */
18
+ export async function applyDatastore(
19
+ serverAddress: string,
20
+ token: string,
21
+ input: DatastoreInput,
22
+ ): Promise<string> {
23
+ const datastore = datastoreInputToProto(input);
24
+ const desc = `datastore "${datastore.metadata?.slug ?? ""}" in org "${datastore.metadata?.org ?? ""}"`;
25
+ return withClient(DatastoreCommandController, serverAddress, token, async (client, callOptions) => {
26
+ try {
27
+ const result = await client.apply(datastore, callOptions);
28
+ return toProtoJson(DatastoreSchema, result);
29
+ } catch (err) {
30
+ throw rpcError(err, desc);
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,47 @@
1
+ // Datastore delete path: resolve org/slug → id via the Query controller, then
2
+ // delete via the Command controller, both over a single shared transport. Like
3
+ // McpServer, the command controller takes ApiResourceDeleteInput{resource_id}.
4
+ //
5
+ // This is the only path that destroys collections — record tools can delete
6
+ // individual records but never structure.
7
+
8
+ import { createClient } from "@connectrpc/connect";
9
+ import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
10
+ import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
11
+ import { DatastoreQueryController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/query_pb";
12
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
13
+
14
+ import { withTransport } from "../client.js";
15
+ import { toProtoJson } from "../marshal.js";
16
+ import { rpcError } from "../rpcerr.js";
17
+
18
+ /** Delete a datastore by org and slug, returning the deleted resource as protojson. */
19
+ export async function deleteDatastore(
20
+ serverAddress: string,
21
+ token: string,
22
+ org: string,
23
+ slug: string,
24
+ ): Promise<string> {
25
+ const desc = `datastore "${slug}" in org "${org}"`;
26
+ return withTransport(serverAddress, token, async (transport, callOptions) => {
27
+ const query = createClient(DatastoreQueryController, transport);
28
+ let id: string;
29
+ try {
30
+ const datastore = await query.getByReference(
31
+ { org, kind: ApiResourceKind.datastore, slug },
32
+ callOptions,
33
+ );
34
+ id = datastore.metadata?.id ?? "";
35
+ } catch (err) {
36
+ throw rpcError(err, desc);
37
+ }
38
+
39
+ const command = createClient(DatastoreCommandController, transport);
40
+ try {
41
+ const deleted = await command.delete({ resourceId: id }, callOptions);
42
+ return toProtoJson(DatastoreSchema, deleted);
43
+ } catch (err) {
44
+ throw rpcError(err, desc);
45
+ }
46
+ });
47
+ }
@@ -0,0 +1,32 @@
1
+ // Datastore-definition read path: the single RPC both the get_datastore tool
2
+ // and the datastore resource template delegate to. This is the *structure*
3
+ // surface (collections, constraints, grants); living records are served by the
4
+ // record tools (records/), whose agent-facing summary is describe_datastore.
5
+
6
+ import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
7
+ import { DatastoreQueryController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/query_pb";
8
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
9
+
10
+ import { withClient } from "../client.js";
11
+ import { toProtoJson } from "../marshal.js";
12
+ import { rpcError } from "../rpcerr.js";
13
+
14
+ /** Retrieve a datastore by org and slug, returning its protojson representation. */
15
+ export async function fetchDatastore(
16
+ serverAddress: string,
17
+ token: string,
18
+ org: string,
19
+ slug: string,
20
+ ): Promise<string> {
21
+ return withClient(DatastoreQueryController, serverAddress, token, async (client, callOptions) => {
22
+ try {
23
+ const datastore = await client.getByReference(
24
+ { org, kind: ApiResourceKind.datastore, slug },
25
+ callOptions,
26
+ );
27
+ return toProtoJson(DatastoreSchema, datastore);
28
+ } catch (err) {
29
+ throw rpcError(err, `datastore "${slug}" in org "${org}"`);
30
+ }
31
+ });
32
+ }