@gcszhn/mcp-sentinel-opencode2-plugin 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -32,6 +32,16 @@ can observe agent tool calls, but nothing can execute a host MCP tool
32
32
  programmatically. The adapter therefore runs in connection-pool mode: it reads
33
33
  the server config from the host and lets the shared core own the MCP client.
34
34
 
35
+ The config capture also acts as the reload hook. V2 replays every registered
36
+ MCP transform whenever the registry is rebuilt (config change, another plugin's
37
+ transform, `ctx.mcp.reload()`), so the same callback refreshes the server list
38
+ and calls the core's `reconcileConnections`: servers that were removed, disabled,
39
+ or whose `command`/`cwd`/`env`/`url`/`headers` changed have their cached client
40
+ closed instead of leaking a child process. Servers that are still configured
41
+ keep their connection, and every V2 location's config is passed to the
42
+ reconciliation so one location never closes a connection another one still
43
+ polls.
44
+
35
45
  Unlike V1, V2 config declares servers under `mcp.servers.{name}`:
36
46
 
37
47
  ```jsonc
@@ -15,16 +15,32 @@
15
15
  *
16
16
  * @module
17
17
  */
18
- import type { LogSink, SentinelEvent, SentinelNotifier, SentinelTask } from "@gcszhn/mcp-sentinel-core";
18
+ import type { LogSink, McpConfig, SentinelEvent, SentinelNotifier, SentinelTask } from "@gcszhn/mcp-sentinel-core";
19
19
  /** A single plugin instance's notification and logging channels. */
20
20
  export interface HostBinding {
21
21
  /** Delivers a resolved sentinel to this instance's host session channel. */
22
22
  readonly notifier: SentinelNotifier;
23
23
  /** Writes a structured log entry through this instance's host log channel. */
24
24
  readonly logSink: LogSink;
25
+ /**
26
+ * The MCP servers this instance's location sees, refreshed on every MCP
27
+ * transform replay. Used to reconcile the process-wide connection cache:
28
+ * a connection may only be closed once **no** live instance needs it.
29
+ */
30
+ mcpConfig?: McpConfig;
25
31
  }
26
32
  /** Return the most recently registered binding, if any. */
27
33
  export declare function getActiveHostBinding(): HostBinding | undefined;
34
+ /**
35
+ * The MCP config of every live plugin instance.
36
+ *
37
+ * The core's connection cache is process-wide, while each V2 instance only sees
38
+ * the servers configured for its own location. Reconciliation must therefore
39
+ * run against every location's config; merging by name would drop a server two
40
+ * locations configure differently (for example a project overriding a global
41
+ * server), and closing it would break the other location's polls.
42
+ */
43
+ export declare function getCombinedMcpConfigs(): McpConfig[];
28
44
  /** Number of currently registered plugin instances. */
29
45
  export declare function getHostBindingCount(): number;
30
46
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"host-registry.d.ts","sourceRoot":"","sources":["../src/host-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EACb,gBAAgB,EAChB,YAAY,EACb,MAAM,2BAA2B,CAAC;AAEnC,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAKD,2DAA2D;AAC3D,wBAAgB,oBAAoB,IAAI,WAAW,GAAG,SAAS,CAE9D;AAED,uDAAuD;AACvD,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAEtF;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC,IAAI,CAAC,CAKf;AAQD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAI9D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAahE"}
1
+ {"version":3,"file":"host-registry.d.ts","sourceRoot":"","sources":["../src/host-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,KAAK,EACV,OAAO,EACP,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,YAAY,EACb,MAAM,2BAA2B,CAAC;AAEnC,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAKD,2DAA2D;AAC3D,wBAAgB,oBAAoB,IAAI,WAAW,GAAG,SAAS,CAE9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,IAAI,SAAS,EAAE,CAEnD;AAED,uDAAuD;AACvD,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAEtF;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC,IAAI,CAAC,CAKf;AAQD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAI9D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAahE"}
@@ -22,6 +22,18 @@ const sessionOwners = new Map();
22
22
  export function getActiveHostBinding() {
23
23
  return bindings[bindings.length - 1];
24
24
  }
25
+ /**
26
+ * The MCP config of every live plugin instance.
27
+ *
28
+ * The core's connection cache is process-wide, while each V2 instance only sees
29
+ * the servers configured for its own location. Reconciliation must therefore
30
+ * run against every location's config; merging by name would drop a server two
31
+ * locations configure differently (for example a project overriding a global
32
+ * server), and closing it would break the other location's polls.
33
+ */
34
+ export function getCombinedMcpConfigs() {
35
+ return bindings.map((binding) => binding.mcpConfig ?? { servers: {} });
36
+ }
25
37
  /** Number of currently registered plugin instances. */
26
38
  export function getHostBindingCount() {
27
39
  return bindings.length;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;;AAiK1C,wBAoNG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;;AAmK1C,wBAmOG"}
package/dist/index.js CHANGED
@@ -15,9 +15,9 @@
15
15
  * @module
16
16
  */
17
17
  import { Plugin } from "@opencode/plugin";
18
- import { ACTION_DESCRIPTION, ARGS_DESCRIPTION, ATTACH_ID_DESCRIPTION, ATTACH_TIMEOUT_DESCRIPTION, ATTACH_TOOL_DESCRIPTION, COMPLETION_NOTIFICATION_NOTE, INTERVAL_DESCRIPTION, POLL_TIMEOUT_DESCRIPTION, POLL_TOOL_DESCRIPTION, READ_ID_DESCRIPTION, READ_LIMIT_DESCRIPTION, READ_OFFSET_DESCRIPTION, READ_TOOL_DESCRIPTION, SERVER_DESCRIPTION, STATUS_ID_DESCRIPTION, STATUS_TOOL_DESCRIPTION, TOOL_DESCRIPTION, UNTIL_DESCRIPTION, cleanup, disconnectAll, handleAttach, handlePoll, handleRead, handleStatus, logError, logInfo, logWarn, makeConnectionInvoker, makeServerResolver, } from "@gcszhn/mcp-sentinel-core";
18
+ import { ACTION_DESCRIPTION, ARGS_DESCRIPTION, ATTACH_ID_DESCRIPTION, ATTACH_TIMEOUT_DESCRIPTION, ATTACH_TOOL_DESCRIPTION, COMPLETION_NOTIFICATION_NOTE, INTERVAL_DESCRIPTION, POLL_TIMEOUT_DESCRIPTION, POLL_TOOL_DESCRIPTION, READ_ID_DESCRIPTION, READ_LIMIT_DESCRIPTION, READ_OFFSET_DESCRIPTION, READ_TOOL_DESCRIPTION, SERVER_DESCRIPTION, STATUS_ID_DESCRIPTION, STATUS_TOOL_DESCRIPTION, TOOL_DESCRIPTION, UNTIL_DESCRIPTION, cleanup, disconnectAll, handleAttach, handlePoll, handleRead, handleStatus, logError, logInfo, logWarn, makeConnectionInvoker, makeServerResolver, reconcileConnections, } from "@gcszhn/mcp-sentinel-core";
19
19
  import { describeServerProblem, normalizeOpencode2McpConfig } from "./config.js";
20
- import { claimSession, getHostBindingCount, registerHostBinding, unregisterHostBinding, } from "./host-registry.js";
20
+ import { claimSession, getCombinedMcpConfigs, getHostBindingCount, registerHostBinding, unregisterHostBinding, } from "./host-registry.js";
21
21
  import { buildNotificationText } from "./notify.js";
22
22
  /** Stable plugin identifier used by OpenCode diagnostics and plugin storage. */
23
23
  const PLUGIN_ID = "mcp-sentinel-opencode2";
@@ -97,14 +97,6 @@ function isNonNegativeInteger(value) {
97
97
  export default Plugin.define({
98
98
  id: PLUGIN_ID,
99
99
  async setup(ctx) {
100
- // V2 exposes MCP *configuration* only inside a transform; `ctx.mcp.list()`
101
- // returns connection status, not config. Capture the current entries from
102
- // this cheap, synchronous, replayable callback. OpenCode replays it when
103
- // the registry is rebuilt (for example after a config reload).
104
- let mcpEntries = [];
105
- await ctx.mcp.transform((editor) => {
106
- mcpEntries = editor.list();
107
- });
108
100
  const delivery = ctx.options.delivery === "steer" ? "steer" : "queue";
109
101
  const resume = ctx.options.resume !== false;
110
102
  const logSink = (level, message, extra) => {
@@ -158,6 +150,20 @@ export default Plugin.define({
158
150
  };
159
151
  const binding = { notifier, logSink };
160
152
  registerHostBinding(binding);
153
+ // V2 exposes MCP *configuration* only inside a transform; `ctx.mcp.list()`
154
+ // returns connection status, not config. The callback is replayed whenever
155
+ // OpenCode rebuilds the MCP registry (config reload, another plugin's
156
+ // transform, `ctx.mcp.reload()`), so it doubles as the reload hook: refresh
157
+ // this location's config snapshot and reconcile the process-wide connection
158
+ // cache. Reconcile is fire-and-forget because the callback is synchronous.
159
+ let mcpEntries = [];
160
+ await ctx.mcp.transform((editor) => {
161
+ mcpEntries = editor.list();
162
+ binding.mcpConfig = normalizeOpencode2McpConfig(mcpEntries);
163
+ void reconcileConnections(getCombinedMcpConfigs()).catch((err) => {
164
+ logWarn("Failed to reconcile MCP connections", { error: String(err) });
165
+ });
166
+ });
161
167
  logInfo("MCP Sentinel OpenCode 2 plugin initialized", { version: ctx.app.version });
162
168
  /** Read a server's connection status from the host's MCP domain. */
163
169
  const getMcpStatus = async (name) => {
@@ -294,6 +300,15 @@ ${COMPLETION_NOTIFICATION_NOTE}`,
294
300
  if (getHostBindingCount() === 0) {
295
301
  cleanup();
296
302
  await disconnectAll();
303
+ return;
304
+ }
305
+ // Other locations still share the process-wide connection cache: drop
306
+ // only the connections this location owned.
307
+ try {
308
+ await reconcileConnections(getCombinedMcpConfigs());
309
+ }
310
+ catch (err) {
311
+ logWarn("Failed to reconcile MCP connections on unload", { error: String(err) });
297
312
  }
298
313
  };
299
314
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcszhn/mcp-sentinel-opencode2-plugin",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "OpenCode 2 (V2) plugin that acts as a sentinel between the AI agent and MCP servers — polling long-running tasks on the agent's behalf so that token-costly status loops never enter the LLM inference path",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,7 +19,7 @@
19
19
  "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\""
20
20
  },
21
21
  "dependencies": {
22
- "@gcszhn/mcp-sentinel-core": "1.5.0",
22
+ "@gcszhn/mcp-sentinel-core": "1.6.0",
23
23
  "@opencode/plugin": "2.3.0"
24
24
  },
25
25
  "devDependencies": {