@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,97 @@
1
+ // Unit tests for stigmer:// resource URI parsing and construction.
2
+ // Asserts parity with the Go server's behavior (scheme check, segment counts,
3
+ // version handling, and the kind→authority round-trip).
4
+
5
+ import { describe, expect, it } from "vitest";
6
+
7
+ import {
8
+ buildResourceURI,
9
+ kindToAuthority,
10
+ parseResourceURI,
11
+ parseVersionedResourceURI,
12
+ } from "./resourceuri";
13
+
14
+ describe("parseResourceURI", () => {
15
+ it("extracts org and slug from a two-segment URI", () => {
16
+ expect(parseResourceURI("stigmer://agents/acme/code-reviewer")).toEqual({
17
+ org: "acme",
18
+ slug: "code-reviewer",
19
+ });
20
+ });
21
+
22
+ it("ignores the authority (kind) segment", () => {
23
+ // The authority differs but the org/slug are taken from the path only.
24
+ expect(parseResourceURI("stigmer://mcp-servers/acme/github")).toEqual({
25
+ org: "acme",
26
+ slug: "github",
27
+ });
28
+ });
29
+
30
+ it("rejects a non-stigmer scheme", () => {
31
+ expect(() => parseResourceURI("https://agents/acme/code-reviewer")).toThrow(/expected "stigmer"/);
32
+ });
33
+
34
+ it("rejects the wrong number of segments", () => {
35
+ expect(() => parseResourceURI("stigmer://agents/acme")).toThrow(/got 1/);
36
+ expect(() => parseResourceURI("stigmer://agents/acme/code-reviewer/v1")).toThrow(/got 3/);
37
+ });
38
+
39
+ it("rejects a malformed URI", () => {
40
+ expect(() => parseResourceURI("not a uri")).toThrow(/malformed resource URI/);
41
+ });
42
+ });
43
+
44
+ describe("parseVersionedResourceURI", () => {
45
+ it("treats two segments as latest (empty version)", () => {
46
+ expect(parseVersionedResourceURI("stigmer://skills/acme/my-skill")).toEqual({
47
+ org: "acme",
48
+ slug: "my-skill",
49
+ version: "",
50
+ });
51
+ });
52
+
53
+ it("extracts an explicit version from three segments", () => {
54
+ expect(parseVersionedResourceURI("stigmer://skills/acme/my-skill/v1.2.0")).toEqual({
55
+ org: "acme",
56
+ slug: "my-skill",
57
+ version: "v1.2.0",
58
+ });
59
+ });
60
+
61
+ it("rejects more than three segments", () => {
62
+ expect(() => parseVersionedResourceURI("stigmer://skills/acme/my-skill/v1/extra")).toThrow(
63
+ /got 4/,
64
+ );
65
+ });
66
+
67
+ it("rejects a single segment", () => {
68
+ expect(() => parseVersionedResourceURI("stigmer://skills/acme")).toThrow(/got 1/);
69
+ });
70
+ });
71
+
72
+ describe("buildResourceURI", () => {
73
+ it("builds a URI for every templated kind", () => {
74
+ expect(buildResourceURI("agent", "acme", "a")).toBe("stigmer://agents/acme/a");
75
+ expect(buildResourceURI("mcp_server", "acme", "m")).toBe("stigmer://mcp-servers/acme/m");
76
+ expect(buildResourceURI("skill", "acme", "s")).toBe("stigmer://skills/acme/s");
77
+ expect(buildResourceURI("workflow", "acme", "w")).toBe("stigmer://workflows/acme/w");
78
+ });
79
+
80
+ it("round-trips with parseResourceURI", () => {
81
+ const uri = buildResourceURI("agent", "acme", "code-reviewer");
82
+ expect(parseResourceURI(uri)).toEqual({ org: "acme", slug: "code-reviewer" });
83
+ });
84
+
85
+ it("returns empty string for an unknown kind", () => {
86
+ expect(buildResourceURI("organization", "acme", "x")).toBe("");
87
+ });
88
+
89
+ it("returns empty string when org or slug is empty", () => {
90
+ expect(buildResourceURI("agent", "", "x")).toBe("");
91
+ expect(buildResourceURI("agent", "acme", "")).toBe("");
92
+ });
93
+
94
+ it("covers exactly the four templated kinds", () => {
95
+ expect(Object.keys(kindToAuthority).sort()).toEqual(["agent", "mcp_server", "skill", "workflow"]);
96
+ });
97
+ });
@@ -0,0 +1,124 @@
1
+ // Parsing and construction of stigmer:// resource URIs.
2
+ // Go parity: mcp-server/internal/domains/resourceuri.go.
3
+ //
4
+ // A resource URI has the form stigmer://{authority}/{org}/{slug}[/{version}].
5
+ // The authority (e.g. "agents") is the URL host and encodes the kind; org/slug
6
+ // (and optional version) are the path segments. Parsing deliberately validates
7
+ // only the scheme and the path-segment count/shape — the authority is not
8
+ // checked here, exactly as the Go server behaves.
9
+
10
+ /** The org and slug extracted from a two-segment resource URI. */
11
+ export interface ResourceRef {
12
+ readonly org: string;
13
+ readonly slug: string;
14
+ }
15
+
16
+ /** A resource ref that may also carry an explicit version ("" means latest). */
17
+ export interface VersionedResourceRef extends ResourceRef {
18
+ readonly version: string;
19
+ }
20
+
21
+ /**
22
+ * Extract org and slug from a stigmer://{authority}/{org}/{slug} URI.
23
+ * Throws when the scheme is not "stigmer" or the path is not exactly two
24
+ * non-empty segments.
25
+ */
26
+ export function parseResourceURI(uri: string): ResourceRef {
27
+ const segments = pathSegments(uri);
28
+ if (segments.length !== 2) {
29
+ throw new Error(
30
+ `expected URI path with 2 segments (org/slug), got ${segments.length} in "${uri}"`,
31
+ );
32
+ }
33
+ const org = segments[0] ?? "";
34
+ const slug = segments[1] ?? "";
35
+ if (org === "" || slug === "") {
36
+ throw new Error(`org and slug must be non-empty in "${uri}"`);
37
+ }
38
+ return { org, slug };
39
+ }
40
+
41
+ /**
42
+ * Extract org, slug, and optional version from a resource URI. Two path
43
+ * segments yield version "" (latest); three yield an explicit, non-empty
44
+ * version. Any other shape throws.
45
+ */
46
+ export function parseVersionedResourceURI(uri: string): VersionedResourceRef {
47
+ const segments = pathSegments(uri);
48
+
49
+ let org = "";
50
+ let slug = "";
51
+ let version = "";
52
+ switch (segments.length) {
53
+ case 2:
54
+ org = segments[0] ?? "";
55
+ slug = segments[1] ?? "";
56
+ break;
57
+ case 3:
58
+ org = segments[0] ?? "";
59
+ slug = segments[1] ?? "";
60
+ version = segments[2] ?? "";
61
+ if (version === "") {
62
+ throw new Error(`version segment must be non-empty in "${uri}"`);
63
+ }
64
+ break;
65
+ default:
66
+ throw new Error(
67
+ `expected URI path with 2 or 3 segments (org/slug[/version]), got ${segments.length} in "${uri}"`,
68
+ );
69
+ }
70
+
71
+ if (org === "" || slug === "") {
72
+ throw new Error(`org and slug must be non-empty in "${uri}"`);
73
+ }
74
+ return { org, slug, version };
75
+ }
76
+
77
+ /**
78
+ * Map a singular kind name (as it appears in the ApiResourceKind proto enum) to
79
+ * the plural authority used in stigmer:// URIs. Only kinds that have a
80
+ * registered MCP resource template appear here.
81
+ */
82
+ export const kindToAuthority: Readonly<Record<string, string>> = {
83
+ agent: "agents",
84
+ mcp_server: "mcp-servers",
85
+ skill: "skills",
86
+ workflow: "workflows",
87
+ };
88
+
89
+ /**
90
+ * Construct a stigmer:// URI from a kind, org, and slug — the inverse of
91
+ * {@link parseResourceURI}. Returns "" when the kind has no registered template
92
+ * or org/slug are empty, mirroring the Go server's enrichment fallback.
93
+ */
94
+ export function buildResourceURI(kind: string, org: string, slug: string): string {
95
+ const authority = kindToAuthority[kind];
96
+ if (authority === undefined || org === "" || slug === "") {
97
+ return "";
98
+ }
99
+ return `stigmer://${authority}/${org}/${slug}`;
100
+ }
101
+
102
+ /**
103
+ * Parse the URI, assert the scheme, and return its path split into segments.
104
+ * The double-slash authority (the kind) is intentionally discarded; only the
105
+ * path carries org/slug[/version]. Interior empty segments are preserved so the
106
+ * count matches Go's strings.Split semantics.
107
+ */
108
+ function pathSegments(uri: string): string[] {
109
+ let parsed: URL;
110
+ try {
111
+ parsed = new URL(uri);
112
+ } catch (err) {
113
+ throw new Error(`malformed resource URI "${uri}": ${err instanceof Error ? err.message : err}`);
114
+ }
115
+
116
+ if (parsed.protocol !== "stigmer:") {
117
+ throw new Error(
118
+ `unexpected URI scheme "${parsed.protocol.replace(/:$/, "")}", expected "stigmer"`,
119
+ );
120
+ }
121
+
122
+ const trimmed = parsed.pathname.replace(/^\/+/, "").replace(/\/+$/, "");
123
+ return trimmed === "" ? [] : trimmed.split("/");
124
+ }
@@ -0,0 +1,62 @@
1
+ import { Code, ConnectError } from "@connectrpc/connect";
2
+ import { beforeAll, describe, expect, it } from "vitest";
3
+
4
+ import { configureLogger } from "../logger";
5
+ import { rpcError } from "./rpcerr";
6
+
7
+ beforeAll(() => configureLogger({ level: "error", format: "text" }));
8
+
9
+ const resource = 'agent "code-reviewer" in org "stigmer"';
10
+
11
+ describe("rpcError", () => {
12
+ it("maps NotFound to a slug-guidance message", () => {
13
+ expect(rpcError(new ConnectError("missing", Code.NotFound), resource).message).toBe(
14
+ 'agent "code-reviewer" in org "stigmer" not found. Verify the org and slug are correct.',
15
+ );
16
+ });
17
+
18
+ it("maps PermissionDenied to an API-key-permissions message", () => {
19
+ expect(rpcError(new ConnectError("nope", Code.PermissionDenied), resource).message).toBe(
20
+ 'Permission denied for agent "code-reviewer" in org "stigmer". Check your API key permissions.',
21
+ );
22
+ });
23
+
24
+ it("maps Unauthenticated", () => {
25
+ expect(rpcError(new ConnectError("bad token", Code.Unauthenticated), resource).message).toBe(
26
+ "Authentication failed. Check your API key.",
27
+ );
28
+ });
29
+
30
+ it("maps Unavailable", () => {
31
+ expect(rpcError(new ConnectError("down", Code.Unavailable), resource).message).toBe(
32
+ "Stigmer server is unavailable. Ensure it is running and reachable.",
33
+ );
34
+ });
35
+
36
+ it("maps DeadlineExceeded", () => {
37
+ expect(rpcError(new ConnectError("slow", Code.DeadlineExceeded), resource).message).toBe(
38
+ "Request timed out contacting stigmer-server.",
39
+ );
40
+ });
41
+
42
+ it("passes through the raw message for InvalidArgument", () => {
43
+ expect(rpcError(new ConnectError("field x is required", Code.InvalidArgument), resource).message).toBe(
44
+ "field x is required",
45
+ );
46
+ });
47
+
48
+ it("wraps non-gRPC/unknown errors", () => {
49
+ expect(rpcError(new Error("boom"), resource).message).toBe("unexpected error: boom");
50
+ });
51
+
52
+ it("maps every gRPC code to a non-empty message without throwing", () => {
53
+ // Completeness guard: the default branch must catch any code the explicit
54
+ // switch does not handle, so no gRPC status can ever surface a blank or
55
+ // thrown error to the client.
56
+ const codes = Object.values(Code).filter((c): c is Code => typeof c === "number");
57
+ for (const code of codes) {
58
+ const msg = rpcError(new ConnectError("detail", code), resource).message;
59
+ expect(msg, `code ${Code[code]}`).toBeTruthy();
60
+ }
61
+ });
62
+ });
@@ -0,0 +1,46 @@
1
+ // Translation of transport/RPC failures into user-facing tool errors.
2
+ //
3
+ // Parity contract (mirrors Go internal/domains/rpcerr.go): the original error is
4
+ // logged at WARN for operator debugging; only the classified, user-friendly
5
+ // message is returned to the AI client. Message text is kept verbatim so the
6
+ // TS and Go servers are indistinguishable to clients.
7
+
8
+ import { Code, ConnectError } from "@connectrpc/connect";
9
+ import { log } from "../logger.js";
10
+
11
+ /**
12
+ * Translate a Connect/gRPC error into a user-friendly Error.
13
+ *
14
+ * `resourceDescription` identifies what was being accessed, e.g.
15
+ * `agent "code-reviewer" in org "stigmer"`.
16
+ */
17
+ export function rpcError(err: unknown, resourceDescription: string): Error {
18
+ const ce = ConnectError.from(err);
19
+
20
+ log.warn("rpc call failed", {
21
+ resource: resourceDescription,
22
+ code: Code[ce.code],
23
+ grpc_message: ce.rawMessage,
24
+ });
25
+
26
+ return new Error(classifyCode(ce, resourceDescription));
27
+ }
28
+
29
+ function classifyCode(ce: ConnectError, resourceDescription: string): string {
30
+ switch (ce.code) {
31
+ case Code.NotFound:
32
+ return `${resourceDescription} not found. Verify the org and slug are correct.`;
33
+ case Code.PermissionDenied:
34
+ return `Permission denied for ${resourceDescription}. Check your API key permissions.`;
35
+ case Code.Unauthenticated:
36
+ return "Authentication failed. Check your API key.";
37
+ case Code.Unavailable:
38
+ return "Stigmer server is unavailable. Ensure it is running and reachable.";
39
+ case Code.DeadlineExceeded:
40
+ return "Request timed out contacting stigmer-server.";
41
+ case Code.InvalidArgument:
42
+ return ce.rawMessage;
43
+ default:
44
+ return `unexpected error: ${ce.rawMessage}`;
45
+ }
46
+ }
@@ -0,0 +1,127 @@
1
+ // In-process integration test for the search tool. Verifies resource_uri
2
+ // enrichment for all four searchable kinds, the empty-response short-circuit,
3
+ // the unknown-kind validation error, and pagination passthrough.
4
+
5
+ import { create, toJson } from "@bufbuild/protobuf";
6
+ import type { ConnectRouter } from "@connectrpc/connect";
7
+ import { connectNodeAdapter } from "@connectrpc/connect-node";
8
+ import {
9
+ createServer as createHttp2Server,
10
+ type Http2Server,
11
+ type ServerHttp2Session,
12
+ } from "node:http2";
13
+ import type { AddressInfo } from "node:net";
14
+
15
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
17
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
18
+ import type { PageInfo } from "@stigmer/protos/ai/stigmer/commons/rpc/pagination_pb";
19
+ import {
20
+ type SearchResponse,
21
+ SearchResponseSchema,
22
+ SearchResultSchema,
23
+ } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
24
+ import { SearchService } from "@stigmer/protos/ai/stigmer/search/v1/query_pb";
25
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
26
+
27
+ import { configureLogger } from "../../logger";
28
+ import { createServer } from "../../server";
29
+
30
+ configureLogger({ level: "error", format: "text" });
31
+
32
+ const allKindsResponse = create(SearchResponseSchema, {
33
+ entries: [
34
+ create(SearchResultSchema, { kind: ApiResourceKind.agent, org: "acme", slug: "code-reviewer" }),
35
+ create(SearchResultSchema, { kind: ApiResourceKind.skill, org: "acme", slug: "code-review" }),
36
+ create(SearchResultSchema, { kind: ApiResourceKind.mcp_server, org: "acme", slug: "github" }),
37
+ create(SearchResultSchema, { kind: ApiResourceKind.workflow, org: "acme", slug: "release" }),
38
+ ],
39
+ });
40
+ const emptyResponse = create(SearchResponseSchema, { entries: [] });
41
+
42
+ let backend: Http2Server;
43
+ let client: Client;
44
+ let nextResponse: SearchResponse = allKindsResponse;
45
+ let lastPage: PageInfo | undefined;
46
+ const openSessions = new Set<ServerHttp2Session>();
47
+
48
+ interface ToolResult {
49
+ content: Array<{ type: string; text?: string }>;
50
+ isError?: boolean;
51
+ }
52
+
53
+ async function callSearch(args: Record<string, unknown>): Promise<ToolResult> {
54
+ return (await client.callTool({ name: "search", arguments: args })) as ToolResult;
55
+ }
56
+
57
+ beforeAll(async () => {
58
+ const routes = (router: ConnectRouter) => {
59
+ router.service(SearchService, {
60
+ search: (req) => {
61
+ lastPage = req.page;
62
+ return nextResponse;
63
+ },
64
+ });
65
+ };
66
+ backend = createHttp2Server(connectNodeAdapter({ routes }));
67
+ backend.on("session", (session) => {
68
+ openSessions.add(session);
69
+ session.on("close", () => openSessions.delete(session));
70
+ });
71
+ await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
72
+ const port = (backend.address() as AddressInfo).port;
73
+
74
+ const mcp = createServer({ serverAddress: `127.0.0.1:${port}`, apiKey: "" });
75
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
76
+ client = new Client({ name: "search-integration", version: "test" });
77
+ await Promise.all([mcp.connect(serverTransport), client.connect(clientTransport)]);
78
+ });
79
+
80
+ afterAll(async () => {
81
+ await client?.close();
82
+ for (const session of openSessions) session.destroy();
83
+ await new Promise<void>((resolve) => backend.close(() => resolve()));
84
+ });
85
+
86
+ describe("search tool integration", () => {
87
+ it("enriches every kind with a resource_uri", async () => {
88
+ nextResponse = allKindsResponse;
89
+ const result = await callSearch({ query: "code" });
90
+ expect(result.isError).toBeFalsy();
91
+ const data = JSON.parse(result.content[0]?.text ?? "{}") as {
92
+ entries: Array<{ resource_uri?: string }>;
93
+ };
94
+ expect(data.entries.map((e) => e.resource_uri)).toEqual([
95
+ "stigmer://agents/acme/code-reviewer",
96
+ "stigmer://skills/acme/code-review",
97
+ "stigmer://mcp-servers/acme/github",
98
+ "stigmer://workflows/acme/release",
99
+ ]);
100
+ });
101
+
102
+ it("short-circuits an empty response to the plain marshal", async () => {
103
+ nextResponse = emptyResponse;
104
+ const result = await callSearch({ query: "nothing" });
105
+ expect(result.isError).toBeFalsy();
106
+ expect(JSON.parse(result.content[0]?.text ?? "{}")).toEqual(
107
+ toJson(SearchResponseSchema, emptyResponse, { useProtoFieldName: true }),
108
+ );
109
+ });
110
+
111
+ it("rejects an unknown kind without calling the backend", async () => {
112
+ const result = await callSearch({ kinds: ["bogus"] });
113
+ expect(result.isError).toBe(true);
114
+ expect(result.content[0]?.text).toContain('unknown resource kind "bogus"');
115
+ expect(result.content[0]?.text).toContain("valid kinds: agent, skill, mcp_server, workflow");
116
+ });
117
+
118
+ it("forwards pagination only when requested", async () => {
119
+ nextResponse = allKindsResponse;
120
+ lastPage = undefined;
121
+ await callSearch({ query: "code" });
122
+ expect(lastPage).toBeUndefined();
123
+
124
+ await callSearch({ query: "code", page_size: 5, page_num: 2 });
125
+ expect(lastPage).toMatchObject({ size: 5, num: 2 });
126
+ });
127
+ });
@@ -0,0 +1,160 @@
1
+ // The "search" MCP tool, backed by SearchService.search.
2
+ // Go parity: mcp-server/internal/domains/search/tools.go.
3
+ //
4
+ // One tool covers listing, full-text search, and cross-kind discovery (replacing
5
+ // per-kind list_* tools); the combination of parameters selects the behavior.
6
+ // Each result entry is enriched with a resource_uri that clients can hand
7
+ // straight to resources/read, bridging discovery to read.
8
+
9
+ import { toJson, type MessageInitShape } from "@bufbuild/protobuf";
10
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
12
+ import {
13
+ type SearchRequestSchema,
14
+ type SearchResponse,
15
+ SearchResponseSchema,
16
+ } from "@stigmer/protos/ai/stigmer/search/v1/io_pb";
17
+ import { SearchService } from "@stigmer/protos/ai/stigmer/search/v1/query_pb";
18
+ import { z } from "zod";
19
+
20
+ import { resolveToken, withClient, type BackendTarget } from "../client.js";
21
+ import { toProtoJson } from "../marshal.js";
22
+ import { buildResourceURI } from "../resourceuri.js";
23
+ import { rpcError } from "../rpcerr.js";
24
+ import { textOrError } from "../toolresult.js";
25
+
26
+ /**
27
+ * The searchable kinds and their proto enum values. This is deliberately the
28
+ * four searchable kinds (not every ApiResourceKind), mirroring the Go server's
29
+ * `knownKinds` map and its validation surface.
30
+ */
31
+ const knownKinds: Readonly<Record<string, ApiResourceKind>> = {
32
+ agent: ApiResourceKind.agent,
33
+ skill: ApiResourceKind.skill,
34
+ mcp_server: ApiResourceKind.mcp_server,
35
+ workflow: ApiResourceKind.workflow,
36
+ };
37
+
38
+ interface SearchArgs {
39
+ readonly kinds?: string[];
40
+ readonly query?: string;
41
+ readonly org?: string;
42
+ readonly excludePublic?: boolean;
43
+ readonly pageSize?: number;
44
+ readonly pageNum?: number;
45
+ }
46
+
47
+ /** Register the search tool; returns the registered tool names. */
48
+ export function registerSearchTools(server: McpServer, target: BackendTarget): string[] {
49
+ server.registerTool(
50
+ "search",
51
+ {
52
+ description:
53
+ "Search and list Stigmer resources (agents, skills, MCP servers, workflows). " +
54
+ "Set 'kinds' to filter by resource type. Set 'query' for full-text search. " +
55
+ "Set 'org' to scope to an organization. Omit 'query' to list all accessible resources.",
56
+ inputSchema: {
57
+ kinds: z
58
+ .array(z.string())
59
+ .optional()
60
+ .describe("Resource kinds to search. Valid: agent, skill, mcp_server, workflow. Empty searches all."),
61
+ query: z
62
+ .string()
63
+ .optional()
64
+ .describe("Full-text search query. Empty lists all accessible resources."),
65
+ org: z
66
+ .string()
67
+ .optional()
68
+ .describe("Organization slug to scope the search. Empty searches all accessible orgs."),
69
+ exclude_public: z
70
+ .boolean()
71
+ .optional()
72
+ .describe("Exclude public/platform resources from results."),
73
+ page_size: z.number().int().optional().describe("Results per page (default 20, max 100)."),
74
+ page_num: z.number().int().optional().describe("Page number (1-indexed, default 1)."),
75
+ },
76
+ },
77
+ (args, extra) =>
78
+ textOrError(() =>
79
+ search(target.serverAddress, resolveToken(extra, target.apiKey), {
80
+ kinds: args.kinds,
81
+ query: args.query,
82
+ org: args.org,
83
+ excludePublic: args.exclude_public,
84
+ pageSize: args.page_size,
85
+ pageNum: args.page_num,
86
+ }),
87
+ ),
88
+ );
89
+
90
+ return ["search"];
91
+ }
92
+
93
+ /** Run the search RPC and return the enriched protojson result. */
94
+ async function search(serverAddress: string, token: string, args: SearchArgs): Promise<string> {
95
+ const kinds = parseKinds(args.kinds);
96
+ return withClient(SearchService, serverAddress, token, async (client, callOptions) => {
97
+ const req: MessageInitShape<typeof SearchRequestSchema> = {
98
+ kinds,
99
+ query: args.query ?? "",
100
+ org: args.org ?? "",
101
+ excludePublic: args.excludePublic ?? false,
102
+ };
103
+ // Attach pagination only when explicitly requested, letting the server apply
104
+ // its own defaults otherwise (Go does the same).
105
+ if ((args.pageSize ?? 0) > 0 || (args.pageNum ?? 0) > 0) {
106
+ req.page = { size: args.pageSize ?? 0, num: args.pageNum ?? 0 };
107
+ }
108
+
109
+ let resp: SearchResponse;
110
+ try {
111
+ resp = await client.search(req, callOptions);
112
+ } catch (err) {
113
+ throw rpcError(err, args.org ? `search results in org "${args.org}"` : "search results");
114
+ }
115
+ return enrichSearchResponse(resp);
116
+ });
117
+ }
118
+
119
+ /** Convert user-supplied kind strings to proto enum values, rejecting unknowns. */
120
+ function parseKinds(raw: string[] | undefined): ApiResourceKind[] {
121
+ if (!raw || raw.length === 0) {
122
+ return [];
123
+ }
124
+ return raw.map((s) => {
125
+ const kind = knownKinds[s];
126
+ if (kind === undefined) {
127
+ throw new Error(
128
+ `unknown resource kind "${s}"; valid kinds: agent, skill, mcp_server, workflow`,
129
+ );
130
+ }
131
+ return kind;
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Serialize the response and inject a resource_uri into each entry whose kind
137
+ * has a registered resource template. Empty responses short-circuit to the plain
138
+ * marshal to avoid the walk.
139
+ */
140
+ function enrichSearchResponse(resp: SearchResponse): string {
141
+ if (resp.entries.length === 0) {
142
+ return toProtoJson(SearchResponseSchema, resp);
143
+ }
144
+
145
+ const data = toJson(SearchResponseSchema, resp, { useProtoFieldName: true }) as Record<
146
+ string,
147
+ unknown
148
+ >;
149
+ const entries = Array.isArray(data.entries)
150
+ ? (data.entries as Array<Record<string, unknown>>)
151
+ : [];
152
+ for (let i = 0; i < entries.length && i < resp.entries.length; i++) {
153
+ const r = resp.entries[i]!;
154
+ const uri = buildResourceURI(ApiResourceKind[r.kind] ?? "", r.org, r.slug);
155
+ if (uri !== "") {
156
+ entries[i]!.resource_uri = uri;
157
+ }
158
+ }
159
+ return JSON.stringify(data, null, 2);
160
+ }
@@ -0,0 +1,44 @@
1
+ // Skill delete path: resolve org/slug → id, then delete (removing the skill and
2
+ // all its versions), both over a single shared transport.
3
+ // Go parity: mcp-server/internal/domains/skills/delete.go.
4
+
5
+ import { createClient } from "@connectrpc/connect";
6
+ import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
7
+ import { SkillCommandController } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/command_pb";
8
+ import { SkillQueryController } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/query_pb";
9
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
10
+
11
+ import { withTransport } from "../client.js";
12
+ import { toProtoJson } from "../marshal.js";
13
+ import { rpcError } from "../rpcerr.js";
14
+
15
+ /**
16
+ * Delete a skill and all its versions by org and slug, returning the deleted
17
+ * skill as protojson.
18
+ */
19
+ export async function deleteSkill(
20
+ serverAddress: string,
21
+ token: string,
22
+ org: string,
23
+ slug: string,
24
+ ): Promise<string> {
25
+ const desc = `skill "${slug}" in org "${org}"`;
26
+ return withTransport(serverAddress, token, async (transport, callOptions) => {
27
+ const query = createClient(SkillQueryController, transport);
28
+ let id: string;
29
+ try {
30
+ const skill = await query.getByReference({ org, kind: ApiResourceKind.skill, slug }, callOptions);
31
+ id = skill.metadata?.id ?? "";
32
+ } catch (err) {
33
+ throw rpcError(err, desc);
34
+ }
35
+
36
+ const command = createClient(SkillCommandController, transport);
37
+ try {
38
+ const deleted = await command.delete({ value: id }, callOptions);
39
+ return toProtoJson(SkillSchema, deleted);
40
+ } catch (err) {
41
+ throw rpcError(err, desc);
42
+ }
43
+ });
44
+ }