@stigmer/mcp-server 3.12.0 → 3.12.1

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.
@@ -6,7 +6,10 @@
6
6
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
7
7
  import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
8
8
 
9
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
10
+
9
11
  import { agentInputToProto, type AgentInput } from "../../gen/agent.js";
12
+ import { applyDeclaredVisibility } from "../apply-visibility.js";
10
13
  import { withClient } from "../client.js";
11
14
  import { toProtoJson } from "../marshal.js";
12
15
  import { rpcError } from "../rpcerr.js";
@@ -21,7 +24,13 @@ export async function applyAgent(
21
24
  const desc = `agent "${agent.metadata?.slug ?? ""}" in org "${agent.metadata?.org ?? ""}"`;
22
25
  return withClient(AgentCommandController, serverAddress, token, async (client, callOptions) => {
23
26
  try {
24
- const result = await client.apply(agent, callOptions);
27
+ const applied = await client.apply(agent, callOptions);
28
+ const result = await applyDeclaredVisibility(
29
+ client,
30
+ callOptions,
31
+ applied,
32
+ agent.metadata?.visibility ?? ApiResourceVisibility.api_resource_visibility_unspecified,
33
+ );
25
34
  return toProtoJson(AgentSchema, result);
26
35
  } catch (err) {
27
36
  throw rpcError(err, desc);
@@ -0,0 +1,47 @@
1
+ // Declared-visibility follow-up for the apply tools (oss#573).
2
+ //
3
+ // Plain updates preserve the stored visibility on both editions — the
4
+ // updateVisibility RPC is the only door for visibility changes, where the
5
+ // server-side guards live (per-kind level support, default-instance
6
+ // rejection). So when an apply tool's input declares a visibility and the
7
+ // applied resource comes back with a different level (i.e. the resource
8
+ // already existed), we follow up with one UpdateVisibility RPC on the same
9
+ // controller. The CLI's skill-push and manifest-apply flows do the same.
10
+ //
11
+ // No-ops are skipped (input omitted visibility, create honored it, or the
12
+ // stored level already matches), so an unchanged apply costs nothing extra.
13
+ // Guard rejections propagate to the caller's rpcError wrapper — the model
14
+ // sees the real FAILED_PRECONDITION / INVALID_ARGUMENT, not a silent lie.
15
+
16
+ import { create } from "@bufbuild/protobuf";
17
+ import type { CallOptions } from "@connectrpc/connect";
18
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
19
+ import { UpdateVisibilityInputSchema, type UpdateVisibilityInput } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
20
+ import type { ApiResourceMetadata } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
21
+
22
+ interface HasResourceMetadata {
23
+ metadata?: ApiResourceMetadata;
24
+ }
25
+
26
+ interface UpdateVisibilityClient<T> {
27
+ updateVisibility(input: UpdateVisibilityInput, options?: CallOptions): Promise<T>;
28
+ }
29
+
30
+ /**
31
+ * Land the input-declared visibility through the guarded door when the
32
+ * applied resource's stored level differs. Returns the resource carrying the
33
+ * final visibility (the follow-up's response, or `applied` unchanged when no
34
+ * follow-up was needed).
35
+ */
36
+ export async function applyDeclaredVisibility<T extends HasResourceMetadata>(
37
+ client: UpdateVisibilityClient<T>,
38
+ callOptions: CallOptions,
39
+ applied: T,
40
+ declared: ApiResourceVisibility,
41
+ ): Promise<T> {
42
+ if (declared === ApiResourceVisibility.api_resource_visibility_unspecified) return applied;
43
+ const resourceId = applied.metadata?.id ?? "";
44
+ if (resourceId === "") return applied;
45
+ if (applied.metadata?.visibility === declared) return applied;
46
+ return client.updateVisibility(create(UpdateVisibilityInputSchema, { resourceId, visibility: declared }), callOptions);
47
+ }
@@ -7,6 +7,7 @@
7
7
  // data map with secret flags, and the recursive workflow task_config expansion
8
8
  // (http_call leaf, fork/for_each nesting).
9
9
 
10
+ import { clone } from "@bufbuild/protobuf";
10
11
  import type { ConnectRouter } from "@connectrpc/connect";
11
12
  import { connectNodeAdapter } from "@connectrpc/connect-node";
12
13
  import {
@@ -18,7 +19,7 @@ import type { AddressInfo } from "node:net";
18
19
 
19
20
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
20
21
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
21
- import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
22
+ import { type Agent, AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
22
23
  import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
23
24
  import type { Environment } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
24
25
  import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
@@ -29,6 +30,7 @@ import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/wo
29
30
  import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
30
31
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
31
32
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
33
+ import type { UpdateVisibilityInput } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
32
34
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
33
35
 
34
36
  import { configureLogger } from "../logger";
@@ -42,6 +44,13 @@ let appliedAgent: Agent | undefined;
42
44
  let appliedMcpServer: McpServerProto | undefined;
43
45
  let appliedWorkflow: Workflow | undefined;
44
46
  let appliedEnvironment: Environment | undefined;
47
+ const visibilityUpdates: UpdateVisibilityInput[] = [];
48
+
49
+ // The single-door contract (oss#573): a plain update preserves the STORED
50
+ // visibility, so the apply response can disagree with the request. This slug
51
+ // makes the mock backend simulate that — apply echoes back a pre-existing
52
+ // resource whose stored level is org, regardless of the requested one.
53
+ const STORED_ORG_SLUG = "stored-org-agent";
45
54
  const openSessions = new Set<ServerHttp2Session>();
46
55
 
47
56
  interface ToolResult {
@@ -58,8 +67,21 @@ beforeAll(async () => {
58
67
  router.service(AgentCommandController, {
59
68
  apply: (req) => {
60
69
  appliedAgent = req;
70
+ if (req.metadata?.slug === STORED_ORG_SLUG) {
71
+ const stored = clone(AgentSchema, req);
72
+ stored.metadata!.id = "agent-stored-1";
73
+ stored.metadata!.visibility = ApiResourceVisibility.visibility_org;
74
+ return stored;
75
+ }
61
76
  return req;
62
77
  },
78
+ updateVisibility: (input) => {
79
+ visibilityUpdates.push(input);
80
+ const updated = clone(AgentSchema, appliedAgent!);
81
+ updated.metadata!.id = input.resourceId;
82
+ updated.metadata!.visibility = input.visibility;
83
+ return updated;
84
+ },
63
85
  });
64
86
  router.service(McpServerCommandController, {
65
87
  apply: (req) => {
@@ -234,6 +256,38 @@ describe("apply tools integration", () => {
234
256
  expect(forCfg.do).toHaveLength(1);
235
257
  });
236
258
 
259
+ it("apply_agent lands a declared visibility the update preserved away via UpdateVisibility (oss#573)", async () => {
260
+ const result = await callTool("apply_agent", {
261
+ name: "Stored Org Agent",
262
+ org: "acme",
263
+ visibility: "PUBLIC",
264
+ instructions: "i",
265
+ });
266
+ expect(result.isError).toBeFalsy();
267
+
268
+ // Server "preserved" stored org; the tool must follow up once through
269
+ // the guarded door and return the landed level, not the stale one.
270
+ expect(visibilityUpdates).toHaveLength(1);
271
+ expect(visibilityUpdates[0].resourceId).toBe("agent-stored-1");
272
+ expect(visibilityUpdates[0].visibility).toBe(ApiResourceVisibility.visibility_public);
273
+ const text = result.content.find((c) => c.type === "text")?.text ?? "";
274
+ expect(JSON.parse(text).metadata.visibility).toBe("visibility_public");
275
+ });
276
+
277
+ it("apply_agent skips the follow-up when the server already matches", async () => {
278
+ visibilityUpdates.length = 0;
279
+ const result = await callTool("apply_agent", {
280
+ name: "Plain Agent",
281
+ org: "acme",
282
+ visibility: "PUBLIC",
283
+ instructions: "i",
284
+ });
285
+ expect(result.isError).toBeFalsy();
286
+ // The echo backend returns the requested level (and no id) — no diff,
287
+ // no follow-up.
288
+ expect(visibilityUpdates).toHaveLength(0);
289
+ });
290
+
237
291
  it("apply_environment rebuilds the data map with secret flags", async () => {
238
292
  const result = await callTool("apply_environment", {
239
293
  name: "GitHub Creds",
@@ -11,7 +11,10 @@
11
11
  import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
12
12
  import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
13
13
 
14
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
15
+
14
16
  import { environmentInputToProto, type EnvironmentInput } from "../../gen/environment.js";
17
+ import { applyDeclaredVisibility } from "../apply-visibility.js";
15
18
  import { withClient } from "../client.js";
16
19
  import { toProtoJson } from "../marshal.js";
17
20
  import { rpcError } from "../rpcerr.js";
@@ -30,7 +33,13 @@ export async function applyEnvironment(
30
33
  token,
31
34
  async (client, callOptions) => {
32
35
  try {
33
- const result = await client.apply(environment, callOptions);
36
+ const applied = await client.apply(environment, callOptions);
37
+ const result = await applyDeclaredVisibility(
38
+ client,
39
+ callOptions,
40
+ applied,
41
+ environment.metadata?.visibility ?? ApiResourceVisibility.api_resource_visibility_unspecified,
42
+ );
34
43
  return toProtoJson(EnvironmentSchema, result);
35
44
  } catch (err) {
36
45
  throw rpcError(err, desc);
@@ -7,7 +7,10 @@
7
7
  import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
8
8
  import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
9
9
 
10
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
11
+
10
12
  import { mcpServerInputToProto, type McpServerInput } from "../../gen/mcpserver.js";
13
+ import { applyDeclaredVisibility } from "../apply-visibility.js";
11
14
  import { withClient } from "../client.js";
12
15
  import { toProtoJson } from "../marshal.js";
13
16
  import { rpcError } from "../rpcerr.js";
@@ -26,7 +29,13 @@ export async function applyMcpServer(
26
29
  token,
27
30
  async (client, callOptions) => {
28
31
  try {
29
- const result = await client.apply(server, callOptions);
32
+ const applied = await client.apply(server, callOptions);
33
+ const result = await applyDeclaredVisibility(
34
+ client,
35
+ callOptions,
36
+ applied,
37
+ server.metadata?.visibility ?? ApiResourceVisibility.api_resource_visibility_unspecified,
38
+ );
30
39
  return toProtoJson(McpServerSchema, result);
31
40
  } catch (err) {
32
41
  throw rpcError(err, desc);
@@ -11,7 +11,10 @@
11
11
  import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
12
12
  import { WorkflowCommandController } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/command_pb";
13
13
 
14
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
15
+
14
16
  import { workflowInputToProto, type WorkflowInput } from "../../gen/workflow.js";
17
+ import { applyDeclaredVisibility } from "../apply-visibility.js";
15
18
  import { withClient } from "../client.js";
16
19
  import { toProtoJson } from "../marshal.js";
17
20
  import { rpcError } from "../rpcerr.js";
@@ -30,7 +33,13 @@ export async function applyWorkflow(
30
33
  token,
31
34
  async (client, callOptions) => {
32
35
  try {
33
- const result = await client.apply(workflow, callOptions);
36
+ const applied = await client.apply(workflow, callOptions);
37
+ const result = await applyDeclaredVisibility(
38
+ client,
39
+ callOptions,
40
+ applied,
41
+ workflow.metadata?.visibility ?? ApiResourceVisibility.api_resource_visibility_unspecified,
42
+ );
34
43
  return toProtoJson(WorkflowSchema, result);
35
44
  } catch (err) {
36
45
  throw rpcError(err, desc);
package/src/gen/agent.ts CHANGED
@@ -18,7 +18,7 @@ export const AgentInputShape = {
18
18
  name: z.string().describe("Human-readable name of the resource."),
19
19
  slug: z.string().optional().describe("URL-friendly identifier (lowercase alphanumeric with hyphens). Auto-generated from name if omitted."),
20
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."),
21
+ visibility: z.string().optional().describe("Resource visibility: PRIVATE or PUBLIC. Applied at create; on updates a changed value is landed through the guarded UpdateVisibility RPC. Omit to leave unchanged."),
22
22
  labels: z.record(z.string()).optional().describe("Key-value labels for organization and filtering."),
23
23
  tags: z.array(z.string()).optional().describe("Tags for categorization and discovery."),
24
24
  description: z.string().optional().describe("Human-readable description for UI and marketplace display."),
@@ -15,7 +15,7 @@ export const EnvironmentInputShape = {
15
15
  name: z.string().describe("Human-readable name of the resource."),
16
16
  slug: z.string().optional().describe("URL-friendly identifier (lowercase alphanumeric with hyphens). Auto-generated from name if omitted."),
17
17
  org: z.string().describe("Organization that owns this resource (e.g. acme)."),
18
- visibility: z.string().optional().describe("Resource visibility: PRIVATE or PUBLIC. Omit to leave unchanged on updates."),
18
+ visibility: z.string().optional().describe("Resource visibility: PRIVATE or PUBLIC. Applied at create; on updates a changed value is landed through the guarded UpdateVisibility RPC. Omit to leave unchanged."),
19
19
  labels: z.record(z.string()).optional().describe("Key-value labels for organization and filtering."),
20
20
  tags: z.array(z.string()).optional().describe("Tags for categorization and discovery."),
21
21
  description: z.string().optional().describe("Human-readable description for UI and listing display."),
@@ -18,7 +18,7 @@ export const McpServerInputShape = {
18
18
  name: z.string().describe("Human-readable name of the resource."),
19
19
  slug: z.string().optional().describe("URL-friendly identifier (lowercase alphanumeric with hyphens). Auto-generated from name if omitted."),
20
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."),
21
+ visibility: z.string().optional().describe("Resource visibility: PRIVATE or PUBLIC. Applied at create; on updates a changed value is landed through the guarded UpdateVisibility RPC. Omit to leave unchanged."),
22
22
  labels: z.record(z.string()).optional().describe("Key-value labels for organization and filtering."),
23
23
  tags: z.array(z.string()).optional().describe("Tags for categorization and discovery."),
24
24
  description: z.string().optional().describe("Human-readable description for marketplace display and documentation. Should explain what this MCP server does and its primary use cases. Example: 'GitHub MCP server for repository operations, code search, and PR management'"),
@@ -45,7 +45,7 @@ export const WorkflowInputShape = {
45
45
  name: z.string().describe("Human-readable name of the resource."),
46
46
  slug: z.string().optional().describe("URL-friendly identifier (lowercase alphanumeric with hyphens). Auto-generated from name if omitted."),
47
47
  org: z.string().describe("Organization that owns this resource (e.g. acme)."),
48
- visibility: z.string().optional().describe("Resource visibility: PRIVATE or PUBLIC. Omit to leave unchanged on updates."),
48
+ visibility: z.string().optional().describe("Resource visibility: PRIVATE or PUBLIC. Applied at create; on updates a changed value is landed through the guarded UpdateVisibility RPC. Omit to leave unchanged."),
49
49
  labels: z.record(z.string()).optional().describe("Key-value labels for organization and filtering."),
50
50
  tags: z.array(z.string()).optional().describe("Tags for categorization and discovery."),
51
51
  description: z.string().optional().describe("Human-readable description for UI and marketplace display."),