@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 +10 -0
- package/dist/host-registry.d.ts +17 -1
- package/dist/host-registry.d.ts.map +1 -1
- package/dist/host-registry.js +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -10
- package/package.json +2 -2
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
|
package/dist/host-registry.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/host-registry.js
CHANGED
|
@@ -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;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;;
|
|
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.
|
|
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.
|
|
22
|
+
"@gcszhn/mcp-sentinel-core": "1.6.0",
|
|
23
23
|
"@opencode/plugin": "2.3.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|