@stigmer/mcp-server 3.0.8-dev.20260612122433

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 (205) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +157 -0
  3. package/cli/mcp-server-stigmer.d.ts +3 -0
  4. package/cli/mcp-server-stigmer.d.ts.map +1 -0
  5. package/cli/mcp-server-stigmer.js +201 -0
  6. package/cli/mcp-server-stigmer.js.map +1 -0
  7. package/config.d.ts +44 -0
  8. package/config.d.ts.map +1 -0
  9. package/config.js +92 -0
  10. package/config.js.map +1 -0
  11. package/domains/agents/apply.d.ts +4 -0
  12. package/domains/agents/apply.d.ts.map +1 -0
  13. package/domains/agents/apply.js +25 -0
  14. package/domains/agents/apply.js.map +1 -0
  15. package/domains/agents/delete.d.ts +3 -0
  16. package/domains/agents/delete.d.ts.map +1 -0
  17. package/domains/agents/delete.js +35 -0
  18. package/domains/agents/delete.js.map +1 -0
  19. package/domains/agents/fetch.d.ts +6 -0
  20. package/domains/agents/fetch.d.ts.map +1 -0
  21. package/domains/agents/fetch.js +24 -0
  22. package/domains/agents/fetch.js.map +1 -0
  23. package/domains/agents/resources.d.ts +5 -0
  24. package/domains/agents/resources.d.ts.map +1 -0
  25. package/domains/agents/resources.js +16 -0
  26. package/domains/agents/resources.js.map +1 -0
  27. package/domains/agents/tools.d.ts +5 -0
  28. package/domains/agents/tools.d.ts.map +1 -0
  29. package/domains/agents/tools.js +41 -0
  30. package/domains/agents/tools.js.map +1 -0
  31. package/domains/client.d.ts +53 -0
  32. package/domains/client.d.ts.map +1 -0
  33. package/domains/client.js +62 -0
  34. package/domains/client.js.map +1 -0
  35. package/domains/marshal.d.ts +8 -0
  36. package/domains/marshal.d.ts.map +1 -0
  37. package/domains/marshal.js +17 -0
  38. package/domains/marshal.js.map +1 -0
  39. package/domains/mcpservers/apply.d.ts +4 -0
  40. package/domains/mcpservers/apply.d.ts.map +1 -0
  41. package/domains/mcpservers/apply.js +26 -0
  42. package/domains/mcpservers/apply.js.map +1 -0
  43. package/domains/mcpservers/delete.d.ts +6 -0
  44. package/domains/mcpservers/delete.d.ts.map +1 -0
  45. package/domains/mcpservers/delete.js +42 -0
  46. package/domains/mcpservers/delete.js.map +1 -0
  47. package/domains/mcpservers/fetch.d.ts +7 -0
  48. package/domains/mcpservers/fetch.d.ts.map +1 -0
  49. package/domains/mcpservers/fetch.js +26 -0
  50. package/domains/mcpservers/fetch.js.map +1 -0
  51. package/domains/mcpservers/resources.d.ts +5 -0
  52. package/domains/mcpservers/resources.d.ts.map +1 -0
  53. package/domains/mcpservers/resources.js +16 -0
  54. package/domains/mcpservers/resources.js.map +1 -0
  55. package/domains/mcpservers/tools.d.ts +5 -0
  56. package/domains/mcpservers/tools.d.ts.map +1 -0
  57. package/domains/mcpservers/tools.js +39 -0
  58. package/domains/mcpservers/tools.js.map +1 -0
  59. package/domains/resourcehandler.d.ts +27 -0
  60. package/domains/resourcehandler.d.ts.map +1 -0
  61. package/domains/resourcehandler.js +32 -0
  62. package/domains/resourcehandler.js.map +1 -0
  63. package/domains/resourceuri.d.ts +34 -0
  64. package/domains/resourceuri.d.ts.map +1 -0
  65. package/domains/resourceuri.js +100 -0
  66. package/domains/resourceuri.js.map +1 -0
  67. package/domains/rpcerr.d.ts +8 -0
  68. package/domains/rpcerr.d.ts.map +1 -0
  69. package/domains/rpcerr.js +42 -0
  70. package/domains/rpcerr.js.map +1 -0
  71. package/domains/search/tools.d.ts +5 -0
  72. package/domains/search/tools.d.ts.map +1 -0
  73. package/domains/search/tools.js +125 -0
  74. package/domains/search/tools.js.map +1 -0
  75. package/domains/skills/delete.d.ts +6 -0
  76. package/domains/skills/delete.d.ts.map +1 -0
  77. package/domains/skills/delete.js +38 -0
  78. package/domains/skills/delete.js.map +1 -0
  79. package/domains/skills/fetch.d.ts +6 -0
  80. package/domains/skills/fetch.d.ts.map +1 -0
  81. package/domains/skills/fetch.js +28 -0
  82. package/domains/skills/fetch.js.map +1 -0
  83. package/domains/skills/resources.d.ts +5 -0
  84. package/domains/skills/resources.d.ts.map +1 -0
  85. package/domains/skills/resources.js +25 -0
  86. package/domains/skills/resources.js.map +1 -0
  87. package/domains/skills/tools.d.ts +5 -0
  88. package/domains/skills/tools.d.ts.map +1 -0
  89. package/domains/skills/tools.js +39 -0
  90. package/domains/skills/tools.js.map +1 -0
  91. package/domains/toolresult.d.ts +12 -0
  92. package/domains/toolresult.d.ts.map +1 -0
  93. package/domains/toolresult.js +30 -0
  94. package/domains/toolresult.js.map +1 -0
  95. package/domains/workflowexecutions/tools.d.ts +5 -0
  96. package/domains/workflowexecutions/tools.d.ts.map +1 -0
  97. package/domains/workflowexecutions/tools.js +80 -0
  98. package/domains/workflowexecutions/tools.js.map +1 -0
  99. package/domains/workflows/apply.d.ts +4 -0
  100. package/domains/workflows/apply.d.ts.map +1 -0
  101. package/domains/workflows/apply.js +30 -0
  102. package/domains/workflows/apply.js.map +1 -0
  103. package/domains/workflows/delete.d.ts +3 -0
  104. package/domains/workflows/delete.d.ts.map +1 -0
  105. package/domains/workflows/delete.js +35 -0
  106. package/domains/workflows/delete.js.map +1 -0
  107. package/domains/workflows/fetch.d.ts +6 -0
  108. package/domains/workflows/fetch.d.ts.map +1 -0
  109. package/domains/workflows/fetch.js +25 -0
  110. package/domains/workflows/fetch.js.map +1 -0
  111. package/domains/workflows/resources.d.ts +5 -0
  112. package/domains/workflows/resources.d.ts.map +1 -0
  113. package/domains/workflows/resources.js +16 -0
  114. package/domains/workflows/resources.js.map +1 -0
  115. package/domains/workflows/taskkinds.d.ts +5 -0
  116. package/domains/workflows/taskkinds.d.ts.map +1 -0
  117. package/domains/workflows/taskkinds.js +66 -0
  118. package/domains/workflows/taskkinds.js.map +1 -0
  119. package/domains/workflows/tools.d.ts +5 -0
  120. package/domains/workflows/tools.d.ts.map +1 -0
  121. package/domains/workflows/tools.js +35 -0
  122. package/domains/workflows/tools.js.map +1 -0
  123. package/domains/workflows/validate.d.ts +5 -0
  124. package/domains/workflows/validate.d.ts.map +1 -0
  125. package/domains/workflows/validate.js +113 -0
  126. package/domains/workflows/validate.js.map +1 -0
  127. package/gen/agent.d.ts +385 -0
  128. package/gen/agent.d.ts.map +1 -0
  129. package/gen/agent.js +170 -0
  130. package/gen/agent.js.map +1 -0
  131. package/gen/apply-runtime.d.ts +18 -0
  132. package/gen/apply-runtime.d.ts.map +1 -0
  133. package/gen/apply-runtime.js +50 -0
  134. package/gen/apply-runtime.js.map +1 -0
  135. package/gen/mcpserver.d.ts +289 -0
  136. package/gen/mcpserver.d.ts.map +1 -0
  137. package/gen/mcpserver.js +166 -0
  138. package/gen/mcpserver.js.map +1 -0
  139. package/gen/workflow.d.ts +805 -0
  140. package/gen/workflow.d.ts.map +1 -0
  141. package/gen/workflow.js +842 -0
  142. package/gen/workflow.js.map +1 -0
  143. package/index.d.ts +20 -0
  144. package/index.d.ts.map +1 -0
  145. package/index.js +58 -0
  146. package/index.js.map +1 -0
  147. package/logger.d.ts +20 -0
  148. package/logger.d.ts.map +1 -0
  149. package/logger.js +41 -0
  150. package/logger.js.map +1 -0
  151. package/package.json +43 -0
  152. package/server.d.ts +60 -0
  153. package/server.d.ts.map +1 -0
  154. package/server.js +366 -0
  155. package/server.js.map +1 -0
  156. package/src/cli/mcp-server-stigmer.ts +42 -0
  157. package/src/config.test.ts +88 -0
  158. package/src/config.ts +151 -0
  159. package/src/domains/agents/apply.ts +30 -0
  160. package/src/domains/agents/delete.ts +41 -0
  161. package/src/domains/agents/fetch.ts +33 -0
  162. package/src/domains/agents/resources.ts +20 -0
  163. package/src/domains/agents/tools.ts +68 -0
  164. package/src/domains/apply.integration.test.ts +220 -0
  165. package/src/domains/client.ts +95 -0
  166. package/src/domains/deletes.integration.test.ts +124 -0
  167. package/src/domains/marshal.ts +21 -0
  168. package/src/domains/mcpservers/apply.ts +36 -0
  169. package/src/domains/mcpservers/delete.ts +51 -0
  170. package/src/domains/mcpservers/fetch.ts +35 -0
  171. package/src/domains/mcpservers/resources.ts +20 -0
  172. package/src/domains/mcpservers/tools.ts +74 -0
  173. package/src/domains/reads.integration.test.ts +134 -0
  174. package/src/domains/resourcehandler.ts +90 -0
  175. package/src/domains/resources.integration.test.ts +139 -0
  176. package/src/domains/resourceuri.test.ts +97 -0
  177. package/src/domains/resourceuri.ts +124 -0
  178. package/src/domains/rpcerr.test.ts +62 -0
  179. package/src/domains/rpcerr.ts +46 -0
  180. package/src/domains/search/search.integration.test.ts +127 -0
  181. package/src/domains/search/tools.ts +160 -0
  182. package/src/domains/skills/delete.ts +44 -0
  183. package/src/domains/skills/fetch.ts +38 -0
  184. package/src/domains/skills/resources.ts +33 -0
  185. package/src/domains/skills/tools.ts +67 -0
  186. package/src/domains/toolresult.ts +33 -0
  187. package/src/domains/workflowexecutions/tools.ts +133 -0
  188. package/src/domains/workflows/apply.ts +40 -0
  189. package/src/domains/workflows/delete.ts +44 -0
  190. package/src/domains/workflows/fetch.ts +34 -0
  191. package/src/domains/workflows/resources.ts +20 -0
  192. package/src/domains/workflows/taskkinds.ts +103 -0
  193. package/src/domains/workflows/tools.ts +68 -0
  194. package/src/domains/workflows/validate.integration.test.ts +117 -0
  195. package/src/domains/workflows/validate.ts +144 -0
  196. package/src/domains/workflows/workflow-tools.integration.test.ts +148 -0
  197. package/src/gen/agent.ts +173 -0
  198. package/src/gen/apply-runtime.ts +52 -0
  199. package/src/gen/mcpserver.ts +163 -0
  200. package/src/gen/workflow.ts +858 -0
  201. package/src/http.integration.test.ts +140 -0
  202. package/src/index.ts +66 -0
  203. package/src/logger.ts +49 -0
  204. package/src/server.integration.test.ts +82 -0
  205. package/src/server.ts +414 -0
@@ -0,0 +1,117 @@
1
+ // In-process integration test for validate_workflow_yaml. Verifies the
2
+ // required-yaml and parse errors, that every registry task kind (including
3
+ // `eval`, which the Go map omits) is accepted, and that the parsed Workflow is
4
+ // forwarded to validateSpec.
5
+
6
+ import { create, toJson } from "@bufbuild/protobuf";
7
+ import type { ConnectRouter } from "@connectrpc/connect";
8
+ import { connectNodeAdapter } from "@connectrpc/connect-node";
9
+ import {
10
+ createServer as createHttp2Server,
11
+ type Http2Server,
12
+ type ServerHttp2Session,
13
+ } from "node:http2";
14
+ import type { AddressInfo } from "node:net";
15
+
16
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
17
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
18
+ import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
19
+ import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
20
+ import { ServerlessWorkflowValidationSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/serverless/validation_pb";
21
+ import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
22
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
23
+ import { stringify as stringifyYaml } from "yaml";
24
+
25
+ import { configureLogger } from "../../logger";
26
+ import { createServer } from "../../server";
27
+
28
+ configureLogger({ level: "error", format: "text" });
29
+
30
+ const registryKinds = Object.values(WorkflowTaskKind).filter(
31
+ (v): v is string => typeof v === "string" && v !== "workflow_task_kind_unspecified",
32
+ );
33
+ const validation = create(ServerlessWorkflowValidationSchema, {});
34
+
35
+ let backend: Http2Server;
36
+ let client: Client;
37
+ let lastWorkflow: Workflow | undefined;
38
+ const openSessions = new Set<ServerHttp2Session>();
39
+
40
+ interface ToolResult {
41
+ content: Array<{ type: string; text?: string }>;
42
+ isError?: boolean;
43
+ }
44
+
45
+ async function validate(yaml: string): Promise<ToolResult> {
46
+ return (await client.callTool({ name: "validate_workflow_yaml", arguments: { yaml } })) as ToolResult;
47
+ }
48
+
49
+ beforeAll(async () => {
50
+ const routes = (router: ConnectRouter) => {
51
+ router.service(WorkflowCommandController, {
52
+ validateSpec: (req) => {
53
+ lastWorkflow = req;
54
+ return validation;
55
+ },
56
+ });
57
+ };
58
+ backend = createHttp2Server(connectNodeAdapter({ routes }));
59
+ backend.on("session", (session) => {
60
+ openSessions.add(session);
61
+ session.on("close", () => openSessions.delete(session));
62
+ });
63
+ await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
64
+ const port = (backend.address() as AddressInfo).port;
65
+
66
+ const mcp = createServer({ serverAddress: `127.0.0.1:${port}`, apiKey: "" });
67
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
68
+ client = new Client({ name: "validate-integration", version: "test" });
69
+ await Promise.all([mcp.connect(serverTransport), client.connect(clientTransport)]);
70
+ });
71
+
72
+ afterAll(async () => {
73
+ await client?.close();
74
+ for (const session of openSessions) session.destroy();
75
+ await new Promise<void>((resolve) => backend.close(() => resolve()));
76
+ });
77
+
78
+ describe("validate_workflow_yaml integration", () => {
79
+ it("requires non-empty yaml", async () => {
80
+ const result = await validate(" ");
81
+ expect(result.isError).toBe(true);
82
+ expect(result.content[0]?.text).toContain("yaml is required");
83
+ });
84
+
85
+ it("rejects yaml without a spec", async () => {
86
+ const result = await validate(stringifyYaml({ apiVersion: "v1", kind: "workflow" }));
87
+ expect(result.isError).toBe(true);
88
+ expect(result.content[0]?.text).toContain(
89
+ "failed to parse workflow YAML: missing or invalid 'spec' field",
90
+ );
91
+ });
92
+
93
+ it("rejects an unknown task kind with its index", async () => {
94
+ const doc = { spec: { tasks: [{ name: "t0", kind: "bogus_kind" }] } };
95
+ const result = await validate(stringifyYaml(doc));
96
+ expect(result.isError).toBe(true);
97
+ expect(result.content[0]?.text).toContain(
98
+ 'failed to parse workflow YAML: unknown task kind "bogus_kind" at tasks[0]',
99
+ );
100
+ });
101
+
102
+ it("accepts every registry task kind (including eval) and forwards the workflow", async () => {
103
+ const doc = {
104
+ apiVersion: "v1",
105
+ kind: "workflow",
106
+ metadata: { name: "wf", org: "acme", slug: "wf" },
107
+ spec: { tasks: registryKinds.map((kind, i) => ({ name: `t${i}`, kind })) },
108
+ };
109
+ const result = await validate(stringifyYaml(doc));
110
+ expect(result.isError).toBeFalsy();
111
+ expect(JSON.parse(result.content[0]?.text ?? "{}")).toEqual(
112
+ toJson(ServerlessWorkflowValidationSchema, validation, { useProtoFieldName: true }),
113
+ );
114
+ expect(lastWorkflow?.spec?.tasks.length).toBe(registryKinds.length);
115
+ expect(lastWorkflow?.spec?.tasks.some((t) => t.kind === WorkflowTaskKind.eval)).toBe(true);
116
+ });
117
+ });
@@ -0,0 +1,144 @@
1
+ // The validate_workflow_yaml tool.
2
+ // Go parity: mcp-server/internal/domains/workflows/validate.go.
3
+ //
4
+ // The YAML is parsed to a Workflow proto locally (string task kinds are checked
5
+ // against the accepted set), then validated server-side via
6
+ // WorkflowCommandController.validateSpec — the same Temporal-based pipeline used
7
+ // by workflow create/update.
8
+ //
9
+ // Parity note (Go D4): the Go server's taskKindNameToEnum map is an identity map
10
+ // that omits `eval`, so Go rejects valid `eval` tasks. We derive the accepted
11
+ // set directly from the WorkflowTaskKind enum's value names instead — identical
12
+ // behavior for the other 19 kinds AND correct for `eval`. Flagged for the Go
13
+ // side as a T03 follow-up.
14
+
15
+ import { fromJson, type JsonValue } from "@bufbuild/protobuf";
16
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import { type Workflow, WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
18
+ import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
19
+ import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
20
+ import { ServerlessWorkflowValidationSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/serverless/validation_pb";
21
+ import { z } from "zod";
22
+ import { parse as parseYaml } from "yaml";
23
+
24
+ import { resolveToken, withClient, type BackendTarget } from "../client.js";
25
+ import { toProtoJson } from "../marshal.js";
26
+ import { rpcError } from "../rpcerr.js";
27
+ import { textOrError } from "../toolresult.js";
28
+
29
+ /**
30
+ * The task kinds the YAML may declare, derived from the proto enum (every value
31
+ * except the unspecified zero value). Deriving from the enum keeps this in lock
32
+ * step with the contract and avoids a hand-maintained list.
33
+ */
34
+ const acceptedTaskKinds: ReadonlySet<string> = new Set(
35
+ Object.values(WorkflowTaskKind).filter(
36
+ (v): v is string => typeof v === "string" && v !== "workflow_task_kind_unspecified",
37
+ ),
38
+ );
39
+
40
+ /** Register the validate_workflow_yaml tool; returns the registered tool names. */
41
+ export function registerValidateWorkflowYamlTool(
42
+ server: McpServer,
43
+ target: BackendTarget,
44
+ ): string[] {
45
+ server.registerTool(
46
+ "validate_workflow_yaml",
47
+ {
48
+ description:
49
+ "Validate a Stigmer workflow YAML for structural and semantic correctness. " +
50
+ "Uses the same Temporal-based validation pipeline as workflow create/update. " +
51
+ "Returns validation state (VALID/INVALID/FAILED), errors, warnings, and the generated internal YAML.",
52
+ inputSchema: {
53
+ yaml: z
54
+ .string()
55
+ .describe(
56
+ "Complete Stigmer workflow YAML content to validate (apiVersion, kind, metadata, spec).",
57
+ ),
58
+ },
59
+ },
60
+ (args, extra) =>
61
+ textOrError(() =>
62
+ validateWorkflowYaml(target.serverAddress, resolveToken(extra, target.apiKey), args.yaml),
63
+ ),
64
+ );
65
+
66
+ return ["validate_workflow_yaml"];
67
+ }
68
+
69
+ /** Parse the YAML to a Workflow proto, then validate it server-side. */
70
+ async function validateWorkflowYaml(
71
+ serverAddress: string,
72
+ token: string,
73
+ yamlContent: string,
74
+ ): Promise<string> {
75
+ if (yamlContent.trim() === "") {
76
+ throw new Error("yaml is required");
77
+ }
78
+
79
+ let workflow: Workflow;
80
+ try {
81
+ workflow = parseWorkflowYaml(yamlContent);
82
+ } catch (err) {
83
+ throw new Error(`failed to parse workflow YAML: ${errMessage(err)}`);
84
+ }
85
+
86
+ return withClient(WorkflowCommandController, serverAddress, token, async (client, callOptions) => {
87
+ try {
88
+ const validation = await client.validateSpec(workflow, callOptions);
89
+ return toProtoJson(ServerlessWorkflowValidationSchema, validation);
90
+ } catch (err) {
91
+ throw rpcError(err, "workflow validation");
92
+ }
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Convert a Stigmer workflow YAML string to a Workflow proto. The YAML has an
98
+ * apiVersion/kind/metadata/spec shape; task kinds are validated against the
99
+ * accepted set. task_config is a google.protobuf.Struct and round-trips through
100
+ * JSON naturally.
101
+ */
102
+ function parseWorkflowYaml(yamlContent: string): Workflow {
103
+ let raw: unknown;
104
+ try {
105
+ raw = parseYaml(yamlContent);
106
+ } catch (err) {
107
+ throw new Error(`invalid YAML syntax: ${errMessage(err)}`);
108
+ }
109
+
110
+ if (!isObject(raw)) {
111
+ throw new Error("missing or invalid 'spec' field");
112
+ }
113
+ const spec = raw.spec;
114
+ if (!isObject(spec)) {
115
+ throw new Error("missing or invalid 'spec' field");
116
+ }
117
+
118
+ const tasks = spec.tasks;
119
+ if (Array.isArray(tasks)) {
120
+ tasks.forEach((task, i) => {
121
+ if (!isObject(task)) {
122
+ return;
123
+ }
124
+ const kind = task.kind;
125
+ if (typeof kind === "string" && kind !== "" && !acceptedTaskKinds.has(kind)) {
126
+ throw new Error(`unknown task kind "${kind}" at tasks[${i}]`);
127
+ }
128
+ });
129
+ }
130
+
131
+ try {
132
+ return fromJson(WorkflowSchema, raw as JsonValue, { ignoreUnknownFields: true });
133
+ } catch (err) {
134
+ throw new Error(`failed to unmarshal into Workflow proto: ${errMessage(err)}`);
135
+ }
136
+ }
137
+
138
+ function isObject(value: unknown): value is Record<string, unknown> {
139
+ return typeof value === "object" && value !== null && !Array.isArray(value);
140
+ }
141
+
142
+ function errMessage(err: unknown): string {
143
+ return err instanceof Error ? err.message : String(err);
144
+ }
@@ -0,0 +1,148 @@
1
+ // In-process integration test for the workflow query tools: the task-kind
2
+ // registry pair (get_task_kind_registry, get_task_kind) and the execution pair
3
+ // (get_workflow_execution, get_workflow_execution_events). Verifies registry
4
+ // selection (case-insensitive), the required-field and not-found errors, and
5
+ // that page_size is forwarded only when set.
6
+
7
+ import { create, toJson } from "@bufbuild/protobuf";
8
+ import type { ConnectRouter } from "@connectrpc/connect";
9
+ import { connectNodeAdapter } from "@connectrpc/connect-node";
10
+ import {
11
+ createServer as createHttp2Server,
12
+ type Http2Server,
13
+ type ServerHttp2Session,
14
+ } from "node:http2";
15
+ import type { AddressInfo } from "node:net";
16
+
17
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
18
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
19
+ import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
20
+ import {
21
+ GetTaskKindRegistryResponseSchema,
22
+ TaskKindDescriptorSchema,
23
+ } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/task_kind_descriptor_pb";
24
+ import { TaskKindRegistryQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/task_kind_registry_query_pb";
25
+ import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
26
+ import {
27
+ type GetEventLogRequest,
28
+ GetEventLogResponseSchema,
29
+ } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/io_pb";
30
+ import { WorkflowExecutionQueryController } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/query_pb";
31
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
32
+
33
+ import { configureLogger } from "../../logger";
34
+ import { createServer } from "../../server";
35
+
36
+ configureLogger({ level: "error", format: "text" });
37
+
38
+ const registry = create(GetTaskKindRegistryResponseSchema, {
39
+ descriptors: [
40
+ create(TaskKindDescriptorSchema, { kind: WorkflowTaskKind.set_vars }),
41
+ create(TaskKindDescriptorSchema, { kind: WorkflowTaskKind.for_each }),
42
+ ],
43
+ });
44
+ const execution = create(WorkflowExecutionSchema, { apiVersion: "v1", kind: "workflow_execution" });
45
+ const eventLog = create(GetEventLogResponseSchema, {});
46
+
47
+ let backend: Http2Server;
48
+ let client: Client;
49
+ let lastExecutionId: string | undefined;
50
+ let lastEventReq: GetEventLogRequest | undefined;
51
+ const openSessions = new Set<ServerHttp2Session>();
52
+
53
+ interface ToolResult {
54
+ content: Array<{ type: string; text?: string }>;
55
+ isError?: boolean;
56
+ }
57
+
58
+ async function callTool(name: string, args: Record<string, unknown>): Promise<ToolResult> {
59
+ return (await client.callTool({ name, arguments: args })) as ToolResult;
60
+ }
61
+
62
+ beforeAll(async () => {
63
+ const routes = (router: ConnectRouter) => {
64
+ router.service(TaskKindRegistryQueryController, { getTaskKindRegistry: () => registry });
65
+ router.service(WorkflowExecutionQueryController, {
66
+ get: (req) => {
67
+ lastExecutionId = req.value;
68
+ return execution;
69
+ },
70
+ getEventLog: (req) => {
71
+ lastEventReq = req;
72
+ return eventLog;
73
+ },
74
+ });
75
+ };
76
+ backend = createHttp2Server(connectNodeAdapter({ routes }));
77
+ backend.on("session", (session) => {
78
+ openSessions.add(session);
79
+ session.on("close", () => openSessions.delete(session));
80
+ });
81
+ await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
82
+ const port = (backend.address() as AddressInfo).port;
83
+
84
+ const mcp = createServer({ serverAddress: `127.0.0.1:${port}`, apiKey: "" });
85
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
86
+ client = new Client({ name: "workflow-tools-integration", version: "test" });
87
+ await Promise.all([mcp.connect(serverTransport), client.connect(clientTransport)]);
88
+ });
89
+
90
+ afterAll(async () => {
91
+ await client?.close();
92
+ for (const session of openSessions) session.destroy();
93
+ await new Promise<void>((resolve) => backend.close(() => resolve()));
94
+ });
95
+
96
+ describe("workflow query tools integration", () => {
97
+ it("get_task_kind_registry returns the full registry", async () => {
98
+ const result = await callTool("get_task_kind_registry", {});
99
+ expect(result.isError).toBeFalsy();
100
+ expect(JSON.parse(result.content[0]?.text ?? "{}")).toEqual(
101
+ toJson(GetTaskKindRegistryResponseSchema, registry, { useProtoFieldName: true }),
102
+ );
103
+ });
104
+
105
+ it("get_task_kind selects one descriptor case-insensitively", async () => {
106
+ const result = await callTool("get_task_kind", { kind: "FOR_EACH" });
107
+ expect(result.isError).toBeFalsy();
108
+ expect(JSON.parse(result.content[0]?.text ?? "{}")).toEqual(
109
+ toJson(TaskKindDescriptorSchema, registry.descriptors[1]!, { useProtoFieldName: true }),
110
+ );
111
+ });
112
+
113
+ it("get_task_kind requires a kind", async () => {
114
+ const result = await callTool("get_task_kind", { kind: "" });
115
+ expect(result.isError).toBe(true);
116
+ expect(result.content[0]?.text).toContain("kind is required");
117
+ });
118
+
119
+ it("get_task_kind reports an unknown kind", async () => {
120
+ const result = await callTool("get_task_kind", { kind: "does_not_exist" });
121
+ expect(result.isError).toBe(true);
122
+ expect(result.content[0]?.text).toContain('task kind "does_not_exist" not found in registry');
123
+ });
124
+
125
+ it("get_workflow_execution requires an execution_id", async () => {
126
+ const result = await callTool("get_workflow_execution", { execution_id: "" });
127
+ expect(result.isError).toBe(true);
128
+ expect(result.content[0]?.text).toContain("execution_id is required");
129
+ });
130
+
131
+ it("get_workflow_execution forwards the id and returns the execution", async () => {
132
+ const result = await callTool("get_workflow_execution", { execution_id: "wex_123" });
133
+ expect(result.isError).toBeFalsy();
134
+ expect(lastExecutionId).toBe("wex_123");
135
+ expect(JSON.parse(result.content[0]?.text ?? "{}")).toEqual(
136
+ toJson(WorkflowExecutionSchema, execution, { useProtoFieldName: true }),
137
+ );
138
+ });
139
+
140
+ it("get_workflow_execution_events forwards page_size only when set", async () => {
141
+ await callTool("get_workflow_execution_events", { execution_id: "wex_123", task_name: "build" });
142
+ expect(lastEventReq?.taskName).toBe("build");
143
+ expect(lastEventReq?.pageSize).toBe(0);
144
+
145
+ await callTool("get_workflow_execution_events", { execution_id: "wex_123", page_size: 50 });
146
+ expect(lastEventReq?.pageSize).toBe(50);
147
+ });
148
+ });
@@ -0,0 +1,173 @@
1
+ // Code generated by stigmer-codegen --target=mcp-ts. DO NOT EDIT.
2
+ //
3
+ // Flattened apply-input zod schema + toProto bridge for the Agent resource.
4
+ // Source proto package: ai.stigmer.agentic.agent.v1
5
+
6
+ import { generateSlug, visibilityFromString } from "./apply-runtime.js";
7
+ import { create } from "@bufbuild/protobuf";
8
+ import { AgentSchema, type Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
9
+ import { AgentSpecSchema, ToolApprovalOverrideSchema, McpServerUsageSchema, McpAccessSchema, SubAgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
10
+ import { EnvVarDeclarationSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
11
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
12
+ import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
13
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
14
+ import { z } from "zod";
15
+
16
+ /** AgentSpec defines the configurable properties of an agent. @internal This is the "Template" layer — declares capabilities and requirements. The overview.md file provides the SDK-facing description and example YAML. */
17
+ export const AgentInputShape = {
18
+ name: z.string().describe("Human-readable name of the resource."),
19
+ slug: z.string().optional().describe("URL-friendly identifier (lowercase alphanumeric with hyphens). Auto-generated from name if omitted."),
20
+ org: z.string().describe("Organization that owns this resource (e.g. acme)."),
21
+ visibility: z.string().optional().describe("Resource visibility: PRIVATE or PUBLIC. Omit to leave unchanged on updates."),
22
+ labels: z.record(z.string()).optional().describe("Key-value labels for organization and filtering."),
23
+ tags: z.array(z.string()).optional().describe("Tags for categorization and discovery."),
24
+ description: z.string().optional().describe("Human-readable description for UI and marketplace display."),
25
+ icon_url: z.string().optional().describe("Icon URL for marketplace and UI display. Must be a publicly accessible URL to an image (SVG, PNG, or JPEG)."),
26
+ instructions: z.string().optional().describe("System prompt defining the agent's behavior and personality."),
27
+ mcp_server_usages: z.array(z.lazy(() => McpServerUsageInputSchema)).optional().describe("MCP servers this agent can use. Each entry must reference a unique McpServer resource by slug."),
28
+ skill_refs: z.array(z.lazy(() => SkillRefInputSchema)).optional().describe("Skill resources providing additional knowledge to the agent."),
29
+ sub_agents: z.array(z.lazy(() => SubAgentInputSchema)).optional().describe("Sub-agents that can be delegated to. Sub-agents can access a subset of the parent's MCP servers and tools."),
30
+ env: z.record(z.lazy(() => EnvVarDeclarationInputSchema)).optional().describe("Environment variable declarations for this agent. Keys are variable names; values describe their metadata and optionality."),
31
+ } as const;
32
+
33
+ export const AgentInputSchema = z.object(AgentInputShape);
34
+ export type AgentInput = z.infer<typeof AgentInputSchema>;
35
+
36
+ const McpServerRefInputSchema = z.object({
37
+ org: z.string().optional().describe("Organization that owns the referenced resource. When non-empty: must be a valid org slug (lowercase alphanumeric with hyphens, starts with a letter, 1-63 characters). Example: 'stigmer', 'acme-corp'. When empty: the reference is relative — the server resolves it to the parent resource's organization at write time. All stored and returned references always have org populated (absolute form). Use empty org for same-org references (the common case). Use explicit org for cross-org references (e.g., marketplace resources)."),
38
+ slug: z.string().describe("Resource slug (user-friendly identifier, unique within org). Format: lowercase alphanumeric with hyphens, must start with a letter and end with a letter or digit (e.g., 'web-search', 'code-reviewer'). Length: 2-63 characters."),
39
+ });
40
+ type McpServerRefInput = z.infer<typeof McpServerRefInputSchema>;
41
+
42
+ const ToolApprovalOverrideInputSchema = z.object({
43
+ tool_name: z.string().optional().describe("Name of the tool to override."),
44
+ requires_approval: z.boolean().optional().describe("Whether this tool requires approval for this agent."),
45
+ message: z.string().optional().describe("Custom approval message shown to the reviewer. Supports {{args.field}} placeholders. When empty, falls back to the McpServer default or auto-generates 'Execute tool: {tool_name}'."),
46
+ });
47
+ type ToolApprovalOverrideInput = z.infer<typeof ToolApprovalOverrideInputSchema>;
48
+
49
+ const McpServerUsageInputSchema = z.object({
50
+ mcp_server_ref: z.lazy(() => McpServerRefInputSchema).describe("Reference to the McpServer resource."),
51
+ enabled_tools: z.array(z.string()).optional().describe("Tools to enable from this MCP server for this agent. Empty list uses the McpServer's default_enabled_tools. Sub-agents can only restrict this set further, not expand it. @internal Tool names must match exactly what the MCP server reports via tools/list. Only names from discovered_capabilities.tools are valid here. Do NOT include names from discovered_capabilities.resource_templates — resource templates are read-only data endpoints, not callable tools. Including a resource template name here causes a fatal runtime error."),
52
+ tool_approval_overrides: z.array(z.lazy(() => ToolApprovalOverrideInputSchema)).optional().describe("Override approval requirements for specific tools. Takes precedence over McpServerSpec.pinned_tool_approvals and McpServerStatus.tool_approvals."),
53
+ });
54
+ type McpServerUsageInput = z.infer<typeof McpServerUsageInputSchema>;
55
+
56
+ const SkillRefInputSchema = z.object({
57
+ org: z.string().optional().describe("Organization that owns the referenced resource. When non-empty: must be a valid org slug (lowercase alphanumeric with hyphens, starts with a letter, 1-63 characters). Example: 'stigmer', 'acme-corp'. When empty: the reference is relative — the server resolves it to the parent resource's organization at write time. All stored and returned references always have org populated (absolute form). Use empty org for same-org references (the common case). Use explicit org for cross-org references (e.g., marketplace resources)."),
58
+ slug: z.string().describe("Resource slug (user-friendly identifier, unique within org). Format: lowercase alphanumeric with hyphens, must start with a letter and end with a letter or digit (e.g., 'web-search', 'code-reviewer'). Length: 2-63 characters."),
59
+ version: z.string().optional().describe("Version of the resource (optional, only applicable to versioned resources like Skills). Supports three formats: 1. Empty/unset → Resolves to 'latest' (most recent version) 2. Tag name → Resolves to version with this tag (e.g., 'stable', 'v1.0') 3. Exact hash → Immutable reference to specific version (e.g., 'abc123...') Default behavior: Empty means 'latest' (current version). This field is ignored for non-versioned resources. Examples: - version: '' → Use latest version - version: 'latest' → Use latest version (explicit) - version: 'stable' → Use version tagged as 'stable' - version: 'v1.0' → Use version tagged as 'v1.0' - version: 'abc123...' → Use exact version with this hash (immutable)"),
60
+ });
61
+ type SkillRefInput = z.infer<typeof SkillRefInputSchema>;
62
+
63
+ const McpAccessInputSchema = z.object({
64
+ mcp_server: z.string().describe("Slug of the McpServer to grant access to. Must match a mcp_server_ref.slug from the parent's mcp_server_usages."),
65
+ enabled_tools: z.array(z.string()).optional().describe("Tools this sub-agent can use from this MCP server. Must be a subset of the parent's enabled_tools for this server. Empty list grants access to all tools the parent has enabled."),
66
+ });
67
+ type McpAccessInput = z.infer<typeof McpAccessInputSchema>;
68
+
69
+ const SubAgentInputSchema = z.object({
70
+ name: z.string().describe("Unique name of the sub-agent within the parent agent."),
71
+ description: z.string().optional().describe("What this sub-agent specializes in."),
72
+ instructions: z.string().optional().describe("System prompt for this sub-agent."),
73
+ mcp_access: z.array(z.lazy(() => McpAccessInputSchema)).optional().describe("MCP server access grants for this sub-agent. Each entry references a parent McpServerUsage by slug and optionally restricts which tools are available."),
74
+ skill_refs: z.array(z.lazy(() => SkillRefInputSchema)).optional().describe("Skill resources for this sub-agent."),
75
+ model_override: z.string().optional().describe("Model override for this sub-agent. When set, uses this model instead of the parent's model. When empty, inherits the parent agent's model."),
76
+ });
77
+ type SubAgentInput = z.infer<typeof SubAgentInputSchema>;
78
+
79
+ const EnvVarDeclarationInputSchema = z.object({
80
+ is_secret: z.boolean().optional().describe("Whether the resolved value should be treated as a secret. @internal When true: encrypted at rest, redacted in logs and Temporal history. When false: stored as plaintext, visible in audit logs."),
81
+ description: z.string().optional().describe("Human-readable description shown in the UI credential form. Should explain what the variable is used for and where to obtain it."),
82
+ optional: z.boolean().optional().describe("Whether this variable is optional. @internal When false (default): the execution pipeline rejects a run if this variable is missing from the user's environment. When true: a missing value is acceptable (the MCP server or agent degrades gracefully without it)."),
83
+ });
84
+ type EnvVarDeclarationInput = z.infer<typeof EnvVarDeclarationInputSchema>;
85
+
86
+
87
+ /** Build the fully-formed Agent proto from the flat MCP apply input. */
88
+ export function agentInputToProto(input: AgentInput): Agent {
89
+ const slug = input.slug && input.slug.length > 0 ? input.slug : generateSlug(input.name);
90
+ const spec = create(AgentSpecSchema);
91
+ if (input.description !== undefined) spec.description = input.description;
92
+ if (input.icon_url !== undefined) spec.iconUrl = input.icon_url;
93
+ if (input.instructions !== undefined) spec.instructions = input.instructions;
94
+ if (input.mcp_server_usages !== undefined) spec.mcpServerUsages = input.mcp_server_usages.map(mcpServerUsageInputToProto);
95
+ if (input.skill_refs !== undefined) spec.skillRefs = input.skill_refs.map(skillRefInputToProto);
96
+ if (input.sub_agents !== undefined) spec.subAgents = input.sub_agents.map(subAgentInputToProto);
97
+ if (input.env !== undefined) {
98
+ for (const [k, v] of Object.entries(input.env)) spec.env[k] = envVarDeclarationInputToProto(v);
99
+ }
100
+ return Object.assign(create(AgentSchema), {
101
+ apiVersion: "agentic.stigmer.ai/v1",
102
+ kind: "Agent",
103
+ metadata: Object.assign(create(ApiResourceMetadataSchema), {
104
+ name: input.name,
105
+ slug,
106
+ org: input.org,
107
+ ...(input.visibility !== undefined && { visibility: visibilityFromString(input.visibility) }),
108
+ ...(input.labels !== undefined && { labels: input.labels }),
109
+ ...(input.tags !== undefined && { tags: input.tags }),
110
+ }),
111
+ spec,
112
+ }) as Agent;
113
+ }
114
+
115
+ function mcpServerRefInputToProto(input: McpServerRefInput) {
116
+ return create(ApiResourceReferenceSchema, {
117
+ org: input.org,
118
+ slug: input.slug,
119
+ kind: ApiResourceKind.mcp_server,
120
+ });
121
+ }
122
+
123
+ function toolApprovalOverrideInputToProto(input: ToolApprovalOverrideInput) {
124
+ const result = create(ToolApprovalOverrideSchema);
125
+ if (input.tool_name !== undefined) result.toolName = input.tool_name;
126
+ if (input.requires_approval !== undefined) result.requiresApproval = input.requires_approval;
127
+ if (input.message !== undefined) result.message = input.message;
128
+ return result;
129
+ }
130
+
131
+ function mcpServerUsageInputToProto(input: McpServerUsageInput) {
132
+ const result = create(McpServerUsageSchema);
133
+ if (input.mcp_server_ref !== undefined) result.mcpServerRef = mcpServerRefInputToProto(input.mcp_server_ref);
134
+ if (input.enabled_tools !== undefined) result.enabledTools = input.enabled_tools;
135
+ if (input.tool_approval_overrides !== undefined) result.toolApprovalOverrides = input.tool_approval_overrides.map(toolApprovalOverrideInputToProto);
136
+ return result;
137
+ }
138
+
139
+ function skillRefInputToProto(input: SkillRefInput) {
140
+ return create(ApiResourceReferenceSchema, {
141
+ org: input.org,
142
+ slug: input.slug,
143
+ kind: ApiResourceKind.skill,
144
+ version: input.version,
145
+ });
146
+ }
147
+
148
+ function mcpAccessInputToProto(input: McpAccessInput) {
149
+ const result = create(McpAccessSchema);
150
+ if (input.mcp_server !== undefined) result.mcpServer = input.mcp_server;
151
+ if (input.enabled_tools !== undefined) result.enabledTools = input.enabled_tools;
152
+ return result;
153
+ }
154
+
155
+ function subAgentInputToProto(input: SubAgentInput) {
156
+ const result = create(SubAgentSchema);
157
+ if (input.name !== undefined) result.name = input.name;
158
+ if (input.description !== undefined) result.description = input.description;
159
+ if (input.instructions !== undefined) result.instructions = input.instructions;
160
+ if (input.mcp_access !== undefined) result.mcpAccess = input.mcp_access.map(mcpAccessInputToProto);
161
+ if (input.skill_refs !== undefined) result.skillRefs = input.skill_refs.map(skillRefInputToProto);
162
+ if (input.model_override !== undefined) result.modelOverride = input.model_override;
163
+ return result;
164
+ }
165
+
166
+ function envVarDeclarationInputToProto(input: EnvVarDeclarationInput) {
167
+ const result = create(EnvVarDeclarationSchema);
168
+ if (input.is_secret !== undefined) result.isSecret = input.is_secret;
169
+ if (input.description !== undefined) result.description = input.description;
170
+ if (input.optional !== undefined) result.optional = input.optional;
171
+ return result;
172
+ }
173
+
@@ -0,0 +1,52 @@
1
+ // Code generated by stigmer-codegen --target=mcp-ts. DO NOT EDIT.
2
+ //
3
+ // Shared runtime helpers for the generated apply-input toProto bridges.
4
+
5
+ import { timestampFromDate, type Timestamp } from "@bufbuild/protobuf/wkt";
6
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
7
+
8
+ /**
9
+ * Slugify a resource name: lowercase, collapse each run of non-alphanumeric
10
+ * characters into a single hyphen, then trim leading/trailing hyphens.
11
+ */
12
+ export function generateSlug(name: string): string {
13
+ if (!name) return "";
14
+ let out = "";
15
+ let lastHyphen = false;
16
+ for (const ch of name.toLowerCase()) {
17
+ if ((ch >= "a" && ch <= "z") || (ch >= "0" && ch <= "9")) {
18
+ out += ch;
19
+ lastHyphen = false;
20
+ } else if (!lastHyphen) {
21
+ out += "-";
22
+ lastHyphen = true;
23
+ }
24
+ }
25
+ return out.replace(/^-+/, "").replace(/-+$/, "");
26
+ }
27
+
28
+ /** Map the PUBLIC/PRIVATE apply input string to the visibility enum. */
29
+ export function visibilityFromString(s: string | undefined): ApiResourceVisibility {
30
+ if (s && s.toUpperCase() === "PUBLIC") return ApiResourceVisibility.visibility_public;
31
+ if (s && s.toUpperCase() === "PRIVATE") return ApiResourceVisibility.visibility_private;
32
+ return ApiResourceVisibility.api_resource_visibility_unspecified;
33
+ }
34
+
35
+ /**
36
+ * Resolve an enum value-name string to its numeric value, leniently: an unknown
37
+ * or missing value yields 0 (the proto UNSPECIFIED sentinel), matching the Go
38
+ * generator's EnumType_value[input] lookup.
39
+ */
40
+ export function enumFromString(
41
+ enumObj: Record<string, string | number>,
42
+ value: string | undefined,
43
+ ): number {
44
+ if (value === undefined) return 0;
45
+ const v = enumObj[value];
46
+ return typeof v === "number" ? v : 0;
47
+ }
48
+
49
+ /** Convert an ISO-8601 string to a protobuf Timestamp message. */
50
+ export function toTimestamp(value: string): Timestamp {
51
+ return timestampFromDate(new Date(value));
52
+ }