mixdog 1.2.0 → 1.2.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.
- package/README.md +7 -5
- package/package.json +1 -1
- package/src/defaults/shipped-history.json +4 -0
- package/src/defaults/skills/pptx/SKILL.md +6 -6
- package/src/defaults/skills/pptx/references/review.md +6 -4
- package/src/defaults/skills/pptx/scripts/source-extract.mjs +1 -1
- package/src/defaults/skills/setup/references/actions.md +13 -3
- package/src/defaults/skills/visualize/SKILL.md +12 -9
- package/src/rules/shared/60-verification.md +3 -1
- package/src/rules/shared/70-delivery.md +4 -2
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +26 -5
- package/src/runtime/agent/orchestrator/config.mjs +10 -4
- package/src/runtime/agent/orchestrator/context/collect.mjs +2 -1
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +11 -5
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +74 -21
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +86 -8
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +5 -1
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +65 -7
- package/src/runtime/agent/orchestrator/mcp/client.mjs +128 -44
- package/src/runtime/agent/orchestrator/mcp/lazy-output-validator.mjs +25 -0
- package/src/runtime/agent/orchestrator/mcp/plugin-placeholders.mjs +54 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +108 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +19 -11
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +337 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-tokens.mjs +159 -0
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +41 -28
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-bridge.mjs +1080 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +15 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +35 -0
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +12 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +6 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +9 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +4 -4
- package/src/runtime/agent/orchestrator/providers/registry.mjs +35 -9
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +15 -55
- package/src/runtime/agent/orchestrator/runtime-core/goal-control.mjs +15 -2
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +267 -84
- package/src/runtime/agent/orchestrator/runtime-core/skill-tool-loading.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-schema.mjs +21 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +54 -3
- package/src/runtime/agent/orchestrator/session/catalog-db/index.mjs +284 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/sqlite-gate.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/worker.mjs +628 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +184 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +136 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +127 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/explicit-skills.mjs +83 -5
- package/src/runtime/agent/orchestrator/session/imported-message.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +6 -4
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +20 -12
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +17 -4
- package/src/runtime/agent/orchestrator/session/manager.mjs +0 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +817 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +19 -12
- package/src/runtime/agent/orchestrator/session/store-summary-reader-catalog-fixture.mjs +158 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +183 -374
- package/src/runtime/agent/orchestrator/session/synthetic-user-envelope.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/tool-batch/pre-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +21 -2
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +38 -17
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +173 -22
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-guards.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +15 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +13 -10
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +17 -12
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/root-resolution.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/patch/apply-patch/route.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/read-redirects.mjs +34 -30
- package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -0
- package/src/runtime/attachments/store.d.mts +3 -0
- package/src/runtime/channels/lib/owned-runtime/owned-start.mjs +1 -1
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +16 -4
- package/src/runtime/channels/lib/worker-main.mjs +17 -14
- package/src/runtime/computer-bridge/linux-readiness.mjs +0 -2
- package/src/runtime/effort-judge/judge-client.mjs +56 -18
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +2 -2
- package/src/runtime/shared/atomic-file.mjs +25 -4
- package/src/runtime/shared/extension-scopes.mjs +5 -3
- package/src/runtime/shared/file-lock.mjs +37 -11
- package/src/runtime/shared/llm/anthropic-effort.mjs +14 -0
- package/src/runtime/shared/llm/model-catalog.mjs +0 -12
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +6 -0
- package/src/runtime/shared/plugin-manifest.mjs +18 -9
- package/src/runtime/shared/pristine-execution-contract.json +9 -2
- package/src/runtime/shared/pristine-execution.mjs +28 -4
- package/src/runtime/shared/provider-accounts.mjs +124 -20
- package/src/runtime/shared/provider-api-key.mjs +12 -0
- package/src/runtime/shared/shell-display.mjs +9 -3
- package/src/runtime/shared/skill-mentions.d.mts +10 -0
- package/src/runtime/shared/skill-mentions.mjs +133 -0
- package/src/runtime/shared/skill-script-runtime.mjs +264 -0
- package/src/runtime/shared/tool-primitives.mjs +35 -2
- package/src/runtime/shared/turn-worktree-snapshot.mjs +25 -5
- package/src/runtime/shared/worker-requests.mjs +15 -2
- package/src/session-runtime/automation-agents.mjs +8 -2
- package/src/session-runtime/boot/apis.mjs +55 -0
- package/src/session-runtime/boot/config.mjs +5 -2
- package/src/session-runtime/boot/facade.mjs +1 -0
- package/src/session-runtime/boot/infrastructure.mjs +29 -18
- package/src/session-runtime/conversation-import/discovery.mjs +698 -0
- package/src/session-runtime/conversation-import/service.mjs +369 -0
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +10 -5
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +47 -42
- package/src/session-runtime/mcp-glue/connect.mjs +57 -10
- package/src/session-runtime/mcp-glue/plugin-servers.mjs +499 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +115 -30
- package/src/session-runtime/mcp-glue.mjs +22 -2
- package/src/session-runtime/new-session-config.mjs +3 -2
- package/src/session-runtime/notification-bus.mjs +9 -2
- package/src/session-runtime/provider-auth/accounts.mjs +33 -15
- package/src/session-runtime/provider-models/web-search-catalog.mjs +5 -1
- package/src/session-runtime/provider-readiness.mjs +16 -10
- package/src/session-runtime/resource-extension-sync.mjs +20 -5
- package/src/session-runtime/resource-mcp-api.mjs +101 -77
- package/src/session-runtime/resource-plugins-api.mjs +29 -41
- package/src/session-runtime/resource-scope-decorate.mjs +6 -8
- package/src/session-runtime/resource-skills-api.mjs +23 -2
- package/src/session-runtime/schedule-session-run.mjs +1 -1
- package/src/session-runtime/services/channel-session-router.mjs +4 -1
- package/src/session-runtime/services/hook-bus/command-handler.mjs +4 -1
- package/src/session-runtime/services/hook-bus/handlers.mjs +7 -3
- package/src/session-runtime/services/provider-admin.mjs +64 -18
- package/src/session-runtime/services/usage-dashboard-model.mjs +3 -0
- package/src/session-runtime/setup-tool/executor.mjs +12 -1
- package/src/session-runtime/setup-tool/extended-actions.mjs +1 -0
- package/src/session-runtime/setup-tool/settings-contract.mjs +1 -0
- package/src/session-runtime/setup-tool/tool-defs.mjs +7 -1
- package/src/session-runtime/skills-api.mjs +7 -3
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +1 -0
- package/src/session-runtime/webhook-session-run.mjs +16 -4
- package/src/skill-script-entry.mjs +36 -0
- package/src/standalone/daemon-stored-session-views.mjs +11 -3
- package/src/standalone/daemon.mjs +41 -4
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +6 -1
- package/src/tui/App.jsx +9 -1
- package/src/tui/app/app-view/floating-panel.jsx +9 -1
- package/src/tui/app/app-view.jsx +6 -0
- package/src/tui/app/extension-pickers/plugins-pickers.mjs +35 -16
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/skill-mention-palette.mjs +88 -0
- package/src/tui/app/slash-palette.mjs +23 -7
- package/src/tui/app/transcript-row-estimate.mjs +11 -3
- package/src/tui/app/use-prompt-draft-flow.mjs +7 -2
- package/src/tui/app/use-prompt-state.mjs +5 -0
- package/src/tui/components/Message.jsx +22 -1
- package/src/tui/components/PromptInput.jsx +21 -3
- package/src/tui/components/SlashCommandPalette.jsx +12 -3
- package/src/tui/components/TranscriptItem.jsx +9 -1
- package/src/tui/components/prompt-input/key-handler.mjs +21 -8
- package/src/tui/components/user-message-skills.mjs +38 -0
- package/src/tui/dist/index.mjs +507 -171
- package/src/tui/session/queue-helpers.mjs +7 -2
- package/src/tui/session/session-api/extensions.mjs +4 -2
- package/src/tui/session/session-api/settings.mjs +7 -0
- package/src/tui/session/session-flow/drain.mjs +14 -4
- package/src/tui/session/tui-steering-persist.mjs +25 -9
- package/src/tui/session/turn.mjs +17 -0
- package/src/runtime/effort-judge/previous-human-request.mjs +0 -162
- package/src/runtime/effort-judge/verified-context-input.mjs +0 -10
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* kind, URL policy, env expansion, stdio environment, HTTP headers, timeouts,
|
|
4
4
|
* and the log scrubbing that keeps credentials out of connection messages.
|
|
5
5
|
*/
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { rememberMcpToolIdentity } from '../../../shared/tool-primitives.mjs';
|
|
6
8
|
|
|
7
9
|
/** Known auto-detect targets: the discovery-advert service name and the MCP
|
|
8
10
|
* endpoint path on its port.
|
|
@@ -108,30 +110,106 @@ export function scrubMcpConnectionMessage(value, cfg = {}) {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
/**
|
|
111
|
-
* Expand `${VAR}` and `${
|
|
112
|
-
* provided env map (defaults to process.env). Recurses into
|
|
113
|
-
*
|
|
113
|
+
* Expand `${VAR}`, `${env:VAR}` and `${VAR:-default}` references in string
|
|
114
|
+
* values using the provided env map (defaults to process.env). Recurses into
|
|
115
|
+
* arrays/objects. The default (possibly empty) applies only when the variable
|
|
116
|
+
* is unset, and runs to the first `}`. An unset variable without a default
|
|
117
|
+
* expands to an empty string and, when `missing` (a Set) is given, its name is
|
|
118
|
+
* recorded there. No shell execution.
|
|
114
119
|
*/
|
|
115
|
-
export function expandEnvVars(value, env = process.env) {
|
|
120
|
+
export function expandEnvVars(value, env = process.env, missing) {
|
|
116
121
|
if (typeof value === 'string') {
|
|
117
|
-
return value.replace(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)
|
|
122
|
+
return value.replace(/\$\{(?:env:)?([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_m, name, fallback) => {
|
|
118
123
|
const v = env?.[name];
|
|
119
|
-
|
|
124
|
+
if (v != null) return String(v);
|
|
125
|
+
if (fallback !== undefined) return fallback;
|
|
126
|
+
missing?.add(name);
|
|
127
|
+
return '';
|
|
120
128
|
});
|
|
121
129
|
}
|
|
122
130
|
if (Array.isArray(value)) {
|
|
123
|
-
return value.map((v) => expandEnvVars(v, env));
|
|
131
|
+
return value.map((v) => expandEnvVars(v, env, missing));
|
|
124
132
|
}
|
|
125
133
|
if (value && typeof value === 'object') {
|
|
126
134
|
const out = {};
|
|
127
135
|
for (const [k, v] of Object.entries(value)) {
|
|
128
|
-
out[k] = expandEnvVars(v, env);
|
|
136
|
+
out[k] = expandEnvVars(v, env, missing);
|
|
129
137
|
}
|
|
130
138
|
return out;
|
|
131
139
|
}
|
|
132
140
|
return value;
|
|
133
141
|
}
|
|
134
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Names (deduplicated, first-seen order) of variables a server config needs
|
|
145
|
+
* that are unset: `${VAR}` references without a default in command, args,
|
|
146
|
+
* env, url, headers and cwd, plus the variables named directly by
|
|
147
|
+
* `bearer_token_env_var`, `env_http_headers` values and `env_vars`. Names
|
|
148
|
+
* only — never values.
|
|
149
|
+
*/
|
|
150
|
+
export function findMissingMcpEnvVars(cfg = {}, env = process.env) {
|
|
151
|
+
const missing = new Set();
|
|
152
|
+
expandEnvVars([cfg.command, cfg.args, cfg.env, cfg.url, cfg.headers, cfg.cwd], env, missing);
|
|
153
|
+
const named = [
|
|
154
|
+
cfg.bearer_token_env_var,
|
|
155
|
+
...(cfg.env_http_headers && typeof cfg.env_http_headers === 'object' && !Array.isArray(cfg.env_http_headers)
|
|
156
|
+
? Object.values(cfg.env_http_headers)
|
|
157
|
+
: []),
|
|
158
|
+
...(Array.isArray(cfg.env_vars) ? cfg.env_vars : []),
|
|
159
|
+
];
|
|
160
|
+
for (const value of named) {
|
|
161
|
+
const name = typeof value === 'string' ? value.trim() : '';
|
|
162
|
+
if (name && envValue(env, name) == null) missing.add(name);
|
|
163
|
+
}
|
|
164
|
+
return [...missing];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Windows variable names are case-insensitive.
|
|
168
|
+
function envValue(env, name) {
|
|
169
|
+
if (process.platform !== 'win32') return env?.[name];
|
|
170
|
+
const actual = Object.keys(env || {}).find((key) => key.toLowerCase() === name.toLowerCase());
|
|
171
|
+
return actual ? env[actual] : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Provider tool names: at most 64 characters of [A-Za-z0-9_-] (the strictest
|
|
175
|
+
// limit among the providers Mixdog talks to).
|
|
176
|
+
const PROVIDER_TOOL_NAME_MAX = 64;
|
|
177
|
+
const PROVIDER_TOOL_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
178
|
+
const KEPT_SERVER_PART_MAX = 40;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* `mcp__<server>__<tool>` when that already fits provider limits (unchanged
|
|
182
|
+
* for every name that fits today). Otherwise a deterministic short form:
|
|
183
|
+
* the server part kept when it is safe (sanitized and cut short when not),
|
|
184
|
+
* the tool part sanitized and cut, and a hash of the full name appended so
|
|
185
|
+
* distinct tools never meet. Callers keep the original tool name alongside
|
|
186
|
+
* to dispatch the call.
|
|
187
|
+
*/
|
|
188
|
+
export function providerSafeMcpToolName(serverName, toolName) {
|
|
189
|
+
const server = String(serverName || '');
|
|
190
|
+
const tool = String(toolName || '');
|
|
191
|
+
const full = `mcp__${server}__${tool}`;
|
|
192
|
+
if (full.length <= PROVIDER_TOOL_NAME_MAX && PROVIDER_TOOL_NAME_RE.test(full)) return full;
|
|
193
|
+
const hash = createHash('sha1').update(full).digest('hex').slice(0, 8);
|
|
194
|
+
const serverPart =
|
|
195
|
+
/^[A-Za-z0-9-]+(?:_[A-Za-z0-9-]+)*$/.test(server) && server.length <= KEPT_SERVER_PART_MAX
|
|
196
|
+
? server
|
|
197
|
+
: server
|
|
198
|
+
.replace(/[^A-Za-z0-9-]+/g, '-')
|
|
199
|
+
.replace(/^-+|-+$/g, '')
|
|
200
|
+
.slice(0, 24)
|
|
201
|
+
.replace(/-+$/, '') || 'server';
|
|
202
|
+
const room = PROVIDER_TOOL_NAME_MAX - `mcp__${serverPart}__`.length - hash.length - 1;
|
|
203
|
+
const toolPart = tool
|
|
204
|
+
.replace(/[^A-Za-z0-9_-]+/g, '_')
|
|
205
|
+
.slice(0, Math.max(0, room))
|
|
206
|
+
.replace(/_+$/, '');
|
|
207
|
+
const safe = `mcp__${serverPart}__${toolPart ? `${toolPart}_` : ''}${hash}`;
|
|
208
|
+
// Name-only consumers in this process (display, guards) resolve it back.
|
|
209
|
+
rememberMcpToolIdentity(safe, server, tool);
|
|
210
|
+
return safe;
|
|
211
|
+
}
|
|
212
|
+
|
|
135
213
|
const DEFAULT_STDIO_ENV_KEYS =
|
|
136
214
|
process.platform === 'win32'
|
|
137
215
|
? [
|
|
@@ -15,12 +15,15 @@ const MAX_MCP_TOOL_PAGES = 100;
|
|
|
15
15
|
* Every tool of a server: follows `nextCursor` until it is exhausted, repeats,
|
|
16
16
|
* or MAX_MCP_TOOL_PAGES is reached. A page with an invalid shape is returned
|
|
17
17
|
* as-is so the caller's shape validation reports it.
|
|
18
|
+
* The SDK rebuilds its output validators from each page alone, so after a
|
|
19
|
+
* paginated listing they are rebuilt once from every page's tools.
|
|
18
20
|
*/
|
|
19
21
|
export async function listAllMcpTools(client, requestOptions = undefined) {
|
|
20
22
|
const tools = [];
|
|
21
23
|
const seen = new Set();
|
|
22
24
|
let cursor = null;
|
|
23
|
-
|
|
25
|
+
let pages = 0;
|
|
26
|
+
for (; pages < MAX_MCP_TOOL_PAGES; pages += 1) {
|
|
24
27
|
const result = await client.listTools(cursor ? { cursor } : undefined, requestOptions);
|
|
25
28
|
if (!result || !Array.isArray(result.tools)) return result;
|
|
26
29
|
tools.push(...result.tools);
|
|
@@ -29,6 +32,7 @@ export async function listAllMcpTools(client, requestOptions = undefined) {
|
|
|
29
32
|
seen.add(next);
|
|
30
33
|
cursor = next;
|
|
31
34
|
}
|
|
35
|
+
if (pages > 0) client.cacheToolMetadata?.(tools);
|
|
32
36
|
return { tools };
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -7,6 +7,7 @@ import { readServicePort } from '../../../shared/service-discovery.mjs';
|
|
|
7
7
|
import {
|
|
8
8
|
AUTO_DETECT_PORTS,
|
|
9
9
|
expandEnvVars,
|
|
10
|
+
findMissingMcpEnvVars,
|
|
10
11
|
mcpUrlForLog,
|
|
11
12
|
normalizeMcpTransportUrl,
|
|
12
13
|
resolveMcpHttpHeaders,
|
|
@@ -48,11 +49,58 @@ function requestInitFor(cfg) {
|
|
|
48
49
|
return headers && Object.keys(headers).length > 0 ? { requestInit: { headers } } : undefined;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// The SDK's WebSocket transport opens its socket without a way to set request
|
|
53
|
+
// headers. This subclass changes only socket creation: Node's WebSocket takes
|
|
54
|
+
// `{ protocols, headers }`; the guard and event wiring match the stock start().
|
|
55
|
+
function createHeaderWebSocketTransport(Base, messageSchema, url, headers) {
|
|
56
|
+
return new (class extends Base {
|
|
57
|
+
start() {
|
|
58
|
+
if (this._socket) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'WebSocketClientTransport already started! If using Client class, note that connect() calls start() automatically.'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
this._socket = new globalThis.WebSocket(this._url, { protocols: 'mcp', headers });
|
|
65
|
+
this._socket.onerror = (event) => {
|
|
66
|
+
const error = 'error' in event ? event.error : new Error(`WebSocket error: ${JSON.stringify(event)}`);
|
|
67
|
+
reject(error);
|
|
68
|
+
this.onerror?.(error);
|
|
69
|
+
};
|
|
70
|
+
this._socket.onopen = () => resolve();
|
|
71
|
+
this._socket.onclose = () => this.onclose?.();
|
|
72
|
+
this._socket.onmessage = (event) => {
|
|
73
|
+
let message;
|
|
74
|
+
try {
|
|
75
|
+
message = messageSchema.parse(JSON.parse(event.data));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.onerror?.(error);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.onmessage?.(message);
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
})(url);
|
|
85
|
+
}
|
|
86
|
+
/** Returns { transport, autoDetectAdvert, stderrTail, warnings } — stderrTail()
|
|
87
|
+
* is the last ~4 KB of a stdio server's (scrubbed) stderr, '' for remote
|
|
88
|
+
* transports. `strictEnv` (the caller passes it for plugin-declared servers)
|
|
89
|
+
* makes unset variables without a default fail the server; otherwise they
|
|
90
|
+
* expand to empty and are reported in `warnings` (names only). */
|
|
91
|
+
export function createMcpTransport({ name, cfg, sdk, log, strictEnv }) {
|
|
54
92
|
const { StdioClientTransport, StreamableHTTPClientTransport, SSEClientTransport, WebSocketClientTransport } = sdk;
|
|
55
93
|
const kind = resolveMcpTransportKind(cfg);
|
|
94
|
+
const warnings = [];
|
|
95
|
+
if (kind !== 'autoDetect') {
|
|
96
|
+
const missing = findMissingMcpEnvVars(cfg);
|
|
97
|
+
if (missing.length) {
|
|
98
|
+
const message = `missing environment variable(s): ${missing.join(', ')}`;
|
|
99
|
+
if (strictEnv === true) throw new Error(message);
|
|
100
|
+
warnings.push(message);
|
|
101
|
+
log(`[mcp-client] "${name}": ${message}\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
56
104
|
if (kind === 'autoDetect') {
|
|
57
105
|
const { url, advert } = resolveAutoDetectUrl(name, cfg);
|
|
58
106
|
log(`[mcp-client] Connecting "${name}" via autoDetect HTTP: ${url}\n`);
|
|
@@ -60,6 +108,7 @@ export function createMcpTransport({ name, cfg, sdk, log }) {
|
|
|
60
108
|
transport: new StreamableHTTPClientTransport(new URL(url)),
|
|
61
109
|
autoDetectAdvert: advert,
|
|
62
110
|
stderrTail: noStderrTail,
|
|
111
|
+
warnings,
|
|
63
112
|
};
|
|
64
113
|
}
|
|
65
114
|
if (kind === 'http' || kind === 'sse') {
|
|
@@ -71,19 +120,28 @@ export function createMcpTransport({ name, cfg, sdk, log }) {
|
|
|
71
120
|
transport: opts ? new Transport(new URL(url), opts) : new Transport(new URL(url)),
|
|
72
121
|
autoDetectAdvert: null,
|
|
73
122
|
stderrTail: noStderrTail,
|
|
123
|
+
warnings,
|
|
74
124
|
};
|
|
75
125
|
}
|
|
76
126
|
if (kind === 'ws') {
|
|
77
|
-
// WebSocketClientTransport ctor takes only a URL; headers are ignored.
|
|
78
127
|
const url = normalizeMcpTransportUrl(expandEnvVars(String(cfg.url ?? '')), 'ws');
|
|
128
|
+
const opts = requestInitFor(cfg);
|
|
79
129
|
log(`[mcp-client] Connecting "${name}" via WebSocket: ${mcpUrlForLog(url)}\n`);
|
|
80
|
-
|
|
130
|
+
const transport = opts
|
|
131
|
+
? createHeaderWebSocketTransport(
|
|
132
|
+
WebSocketClientTransport,
|
|
133
|
+
sdk.JSONRPCMessageSchema,
|
|
134
|
+
new URL(url),
|
|
135
|
+
opts.requestInit.headers
|
|
136
|
+
)
|
|
137
|
+
: new WebSocketClientTransport(new URL(url));
|
|
138
|
+
return { transport, autoDetectAdvert: null, stderrTail: noStderrTail, warnings };
|
|
81
139
|
}
|
|
82
140
|
if (kind === 'stdio') {
|
|
83
141
|
const transport = new StdioClientTransport({
|
|
84
142
|
command: expandEnvVars(String(cfg.command ?? '')),
|
|
85
143
|
args: Array.isArray(cfg.args) ? expandEnvVars(cfg.args) : cfg.args,
|
|
86
|
-
cwd: cfg.cwd,
|
|
144
|
+
cwd: cfg.cwd == null ? cfg.cwd : expandEnvVars(String(cfg.cwd)),
|
|
87
145
|
env: resolveMcpStdioEnvironment(cfg),
|
|
88
146
|
stderr: cfg.stderr ?? 'pipe',
|
|
89
147
|
});
|
|
@@ -93,7 +151,7 @@ export function createMcpTransport({ name, cfg, sdk, log }) {
|
|
|
93
151
|
tail = (tail + text).slice(-MCP_STDERR_TAIL_CHARS);
|
|
94
152
|
log(`[mcp:${name}:stderr] ${text}`);
|
|
95
153
|
});
|
|
96
|
-
return { transport, autoDetectAdvert: null, stderrTail: () => tail.trim() };
|
|
154
|
+
return { transport, autoDetectAdvert: null, stderrTail: () => tail.trim(), warnings };
|
|
97
155
|
}
|
|
98
156
|
throw new Error(
|
|
99
157
|
`Invalid config for "${name}": need autoDetect, type (stdio/http/sse/ws), url (http), or command (stdio)`
|
|
@@ -22,7 +22,13 @@ import { createKeyedSingleflight } from './reconnect-singleflight.mjs';
|
|
|
22
22
|
import { createOwnerFairGate } from '../../../shared/owner-fair-gate.mjs';
|
|
23
23
|
import { currentToolExecutionOwner } from '../../../shared/tool-execution-owner.mjs';
|
|
24
24
|
import { positiveInt } from '../../../shared/numbers.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
providerSafeMcpToolName,
|
|
27
|
+
resolveMcpRequestTimeouts,
|
|
28
|
+
resolveMcpTransportKind,
|
|
29
|
+
scrubMcpConnectionMessage,
|
|
30
|
+
} from './client-config.mjs';
|
|
31
|
+
import { createLazyOutputValidator } from './lazy-output-validator.mjs';
|
|
26
32
|
import { appendMcpStderrTail, createMcpTransport } from './client-transport.mjs';
|
|
27
33
|
import { listAllMcpTools, runBoundedHandshake } from './client-handshake.mjs';
|
|
28
34
|
|
|
@@ -50,6 +56,8 @@ const MCP_TOOL_NAME_RE = /^mcp__(.+?)__(.+)$/;
|
|
|
50
56
|
const _knownMcpScopes = new Set([DEFAULT_MCP_SCOPE_ID]);
|
|
51
57
|
const _connectAbortGenerations = new Map();
|
|
52
58
|
const _pendingConnects = new Set();
|
|
59
|
+
// scopeId -> root path advertised by that scope's scope-following servers.
|
|
60
|
+
const _scopeRoots = new Map();
|
|
53
61
|
let mcpSdkPromise = null;
|
|
54
62
|
// Memo for mcpToolHasField(name, field) — keyed by `${scopeId}|${toolName}|${field}`.
|
|
55
63
|
// The lookup (regex parse + servers Map get + tools.find + schema property
|
|
@@ -58,8 +66,14 @@ let mcpSdkPromise = null;
|
|
|
58
66
|
// mutation point (connectServer / disconnectAll) so a stale positive or
|
|
59
67
|
// negative can never survive a tools-list change.
|
|
60
68
|
const _mcpToolFieldMemo = new Map();
|
|
69
|
+
let _mcpToolGeneration = 0;
|
|
61
70
|
function _invalidateMcpToolFieldMemo() {
|
|
62
71
|
_mcpToolFieldMemo.clear();
|
|
72
|
+
_mcpToolGeneration += 1;
|
|
73
|
+
}
|
|
74
|
+
/** Read-only counter bumped on every MCP tool registry mutation. */
|
|
75
|
+
export function getMcpToolGeneration() {
|
|
76
|
+
return _mcpToolGeneration;
|
|
63
77
|
}
|
|
64
78
|
function normalizeMcpScopeId(value) {
|
|
65
79
|
const raw = value && typeof value === 'object' ? value.scopeId : value;
|
|
@@ -98,12 +112,15 @@ async function loadMcpSdk() {
|
|
|
98
112
|
import('@modelcontextprotocol/sdk/client/sse.js'),
|
|
99
113
|
import('@modelcontextprotocol/sdk/client/websocket.js'),
|
|
100
114
|
import('@modelcontextprotocol/sdk/types.js'),
|
|
101
|
-
|
|
115
|
+
import('@modelcontextprotocol/sdk/validation/ajv'),
|
|
116
|
+
]).then(([clientMod, stdioMod, httpMod, sseMod, wsMod, typesMod, ajvMod]) => ({
|
|
102
117
|
Client: clientMod.Client,
|
|
118
|
+
AjvJsonSchemaValidator: ajvMod.AjvJsonSchemaValidator,
|
|
103
119
|
StdioClientTransport: stdioMod.StdioClientTransport,
|
|
104
120
|
StreamableHTTPClientTransport: httpMod.StreamableHTTPClientTransport,
|
|
105
121
|
SSEClientTransport: sseMod.SSEClientTransport,
|
|
106
122
|
WebSocketClientTransport: wsMod.WebSocketClientTransport,
|
|
123
|
+
JSONRPCMessageSchema: typesMod.JSONRPCMessageSchema,
|
|
107
124
|
ListRootsRequestSchema: typesMod.ListRootsRequestSchema,
|
|
108
125
|
ToolListChangedNotificationSchema: typesMod.ToolListChangedNotificationSchema,
|
|
109
126
|
}));
|
|
@@ -163,8 +180,8 @@ function mcpFeatureTools(serverName, capabilities, protocolTools) {
|
|
|
163
180
|
const tools = [];
|
|
164
181
|
const used = new Set(protocolTools.map((tool) => tool.name));
|
|
165
182
|
const add = (leaf, description, properties, required, operation) => {
|
|
166
|
-
let name =
|
|
167
|
-
|
|
183
|
+
let name = providerSafeMcpToolName(serverName, leaf);
|
|
184
|
+
for (let suffix = '_'; used.has(name); suffix += '_') name = providerSafeMcpToolName(serverName, leaf + suffix);
|
|
168
185
|
used.add(name);
|
|
169
186
|
tools.push({
|
|
170
187
|
name,
|
|
@@ -238,6 +255,7 @@ export function getMcpServerStatus(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
238
255
|
),
|
|
239
256
|
}
|
|
240
257
|
: {}),
|
|
258
|
+
warnings: Array.isArray(server.warnings) ? server.warnings : [],
|
|
241
259
|
toolCount: Array.isArray(server.tools) ? server.tools.length : 0,
|
|
242
260
|
tools: (server.tools || []).map((tool) => ({
|
|
243
261
|
name: tool.name,
|
|
@@ -409,11 +427,12 @@ export async function reconnectSingleMcpServer(scopeId, name, cfg = undefined) {
|
|
|
409
427
|
export async function executeMcpTool(name, args, options = {}) {
|
|
410
428
|
const match = name.match(MCP_TOOL_NAME_RE);
|
|
411
429
|
if (!match) throw new Error(`Not an MCP tool name: ${name}`);
|
|
412
|
-
const [, serverName, toolName] = match;
|
|
413
430
|
const scopeId = normalizeMcpScopeId(options);
|
|
414
|
-
const
|
|
415
|
-
if (!
|
|
416
|
-
const
|
|
431
|
+
const target = resolveMcpToolTarget(scopeId, name);
|
|
432
|
+
if (!target) throw new Error(`MCP server "${match[1]}" not connected`);
|
|
433
|
+
const { server, definition } = target;
|
|
434
|
+
const serverName = server.name;
|
|
435
|
+
const toolName = definition?.mcpToolName || match[2];
|
|
417
436
|
const callSignal = options?.signal || null;
|
|
418
437
|
const dispatch = (target) =>
|
|
419
438
|
definition?.mcpOperation
|
|
@@ -1022,12 +1041,7 @@ export function isMcpTool(name) {
|
|
|
1022
1041
|
}
|
|
1023
1042
|
/** True when the prefixed name exists on a connected MCP server. */
|
|
1024
1043
|
export function isRegisteredMcpTool(name, scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
1025
|
-
|
|
1026
|
-
if (!match) return false;
|
|
1027
|
-
const [, serverName] = match;
|
|
1028
|
-
const server = scopedServer(scopeId, serverName);
|
|
1029
|
-
if (!server || !Array.isArray(server.tools)) return false;
|
|
1030
|
-
return server.tools.some((t) => t?.name === name);
|
|
1044
|
+
return Boolean(resolveMcpToolTarget(scopeId, name)?.definition);
|
|
1031
1045
|
}
|
|
1032
1046
|
/**
|
|
1033
1047
|
* Check whether the inputSchema for an MCP tool declares the given top-level
|
|
@@ -1040,9 +1054,7 @@ export function mcpToolHasField(name, field, scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
1040
1054
|
const memoKey = `${normalizedScopeId}|${name}|${field}`;
|
|
1041
1055
|
const memoized = _mcpToolFieldMemo.get(memoKey);
|
|
1042
1056
|
if (memoized !== undefined) return memoized;
|
|
1043
|
-
const
|
|
1044
|
-
const server = match ? scopedServer(normalizedScopeId, match[1]) : null;
|
|
1045
|
-
const tool = server ? server.tools.find((t) => t.name === name) : null;
|
|
1057
|
+
const tool = resolveMcpToolTarget(normalizedScopeId, name)?.definition;
|
|
1046
1058
|
const props = tool?.inputSchema?.properties;
|
|
1047
1059
|
const result = Boolean(props && Object.hasOwn(props, field));
|
|
1048
1060
|
_mcpToolFieldMemo.set(memoKey, result);
|
|
@@ -1163,8 +1175,14 @@ async function connectServer(
|
|
|
1163
1175
|
// abort before creating a transport/child at all.
|
|
1164
1176
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
1165
1177
|
}
|
|
1166
|
-
const client = createMcpClient(sdk, name, cfg);
|
|
1167
|
-
const { transport, autoDetectAdvert, stderrTail } = createMcpTransport({
|
|
1178
|
+
const client = createMcpClient(sdk, name, cfg, scopeId);
|
|
1179
|
+
const { transport, autoDetectAdvert, stderrTail, warnings } = createMcpTransport({
|
|
1180
|
+
name,
|
|
1181
|
+
cfg,
|
|
1182
|
+
sdk,
|
|
1183
|
+
log: mcpLog,
|
|
1184
|
+
strictEnv: cfg?._mixdogStrictEnv === true,
|
|
1185
|
+
});
|
|
1168
1186
|
const liveness = trackMcpLiveness(client, name, cfg, stderrTail);
|
|
1169
1187
|
const pending = { scopeId, name, client, transport };
|
|
1170
1188
|
_pendingConnects.add(pending);
|
|
@@ -1195,7 +1213,7 @@ async function connectServer(
|
|
|
1195
1213
|
}
|
|
1196
1214
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
1197
1215
|
}
|
|
1198
|
-
const tools = mcpServerTools(name, capabilities, toolsResult.tools);
|
|
1216
|
+
const tools = mcpServerTools(name, capabilities, toolsResult.tools, cfg);
|
|
1199
1217
|
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
1200
1218
|
servers.set(registryKey, {
|
|
1201
1219
|
scopeId,
|
|
@@ -1209,6 +1227,7 @@ async function connectServer(
|
|
|
1209
1227
|
capabilities,
|
|
1210
1228
|
liveness,
|
|
1211
1229
|
stderrTail,
|
|
1230
|
+
warnings: warnings || [],
|
|
1212
1231
|
generation: genAtStart,
|
|
1213
1232
|
});
|
|
1214
1233
|
if (capabilities?.tools?.listChanged && sdk.ToolListChangedNotificationSchema) {
|
|
@@ -1250,16 +1269,25 @@ function trackMcpLiveness(client, name, cfg, stderrTail) {
|
|
|
1250
1269
|
return liveness;
|
|
1251
1270
|
}
|
|
1252
1271
|
|
|
1253
|
-
/** The SDK client for one server, advertising the project root when known.
|
|
1254
|
-
|
|
1255
|
-
|
|
1272
|
+
/** The SDK client for one server, advertising the project root when known.
|
|
1273
|
+
* A config marked `_mixdogRootsFollowScope` advertises its scope's current
|
|
1274
|
+
* root (setMcpScopeRoot) and is told when it changes. */
|
|
1275
|
+
function createMcpClient(sdk, name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
1276
|
+
const followScope = cfg?._mixdogRootsFollowScope === true;
|
|
1277
|
+
const fixedRoot = String(cfg?._mixdogProjectRoot || cfg?.cwd || '').trim();
|
|
1278
|
+
const currentRoot = () => (followScope ? _scopeRoots.get(normalizeMcpScopeId(scopeId)) || fixedRoot : fixedRoot);
|
|
1256
1279
|
const client = new sdk.Client(
|
|
1257
1280
|
{ name: `mixdog-agent/${name}`, version: '1.0.0' },
|
|
1258
|
-
{
|
|
1281
|
+
{
|
|
1282
|
+
capabilities: followScope || fixedRoot ? { roots: { listChanged: followScope } } : {},
|
|
1283
|
+
jsonSchemaValidator: createLazyOutputValidator(sdk.AjvJsonSchemaValidator),
|
|
1284
|
+
}
|
|
1259
1285
|
);
|
|
1260
|
-
if (
|
|
1286
|
+
if ((followScope || fixedRoot) && sdk.ListRootsRequestSchema) {
|
|
1261
1287
|
client.setRequestHandler(sdk.ListRootsRequestSchema, async () => {
|
|
1262
|
-
const
|
|
1288
|
+
const root = currentRoot();
|
|
1289
|
+
if (!root) return { roots: [] };
|
|
1290
|
+
const rootPath = resolve(root);
|
|
1263
1291
|
return {
|
|
1264
1292
|
roots: [
|
|
1265
1293
|
{
|
|
@@ -1273,15 +1301,62 @@ function createMcpClient(sdk, name, cfg) {
|
|
|
1273
1301
|
return client;
|
|
1274
1302
|
}
|
|
1275
1303
|
|
|
1276
|
-
/**
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1304
|
+
/**
|
|
1305
|
+
* Set the root advertised by `scopeId`'s scope-following servers (plugin
|
|
1306
|
+
* servers, whose roots track the session cwd) and notify the connected ones
|
|
1307
|
+
* when it changed. No reconnect: the connection and its tools stay as they are.
|
|
1308
|
+
*/
|
|
1309
|
+
export function setMcpScopeRoot(scopeId, root) {
|
|
1310
|
+
const normalized = normalizeMcpScopeId(scopeId);
|
|
1311
|
+
const next = String(root || '').trim();
|
|
1312
|
+
if ((_scopeRoots.get(normalized) || '') === next) return;
|
|
1313
|
+
if (next) _scopeRoots.set(normalized, next);
|
|
1314
|
+
else _scopeRoots.delete(normalized);
|
|
1315
|
+
for (const [, server] of scopedServerEntries(normalized)) {
|
|
1316
|
+
if (server.cfg?._mixdogRootsFollowScope !== true) continue;
|
|
1317
|
+
Promise.resolve()
|
|
1318
|
+
.then(() => server.client?.sendRootsListChanged?.())
|
|
1319
|
+
.catch(() => {});
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/** The connected server and tool definition a prefixed tool name refers to.
|
|
1324
|
+
* The name's server part finds it directly; a name shortened to fit provider
|
|
1325
|
+
* limits is found by scanning this scope's servers. A known server without
|
|
1326
|
+
* the tool still resolves (definition null) so the call reaches the server. */
|
|
1327
|
+
function resolveMcpToolTarget(scopeId, name) {
|
|
1328
|
+
const match = MCP_TOOL_NAME_RE.exec(String(name || ''));
|
|
1329
|
+
if (!match) return null;
|
|
1330
|
+
const direct = scopedServer(scopeId, match[1]);
|
|
1331
|
+
const directTool = direct?.tools?.find((tool) => tool.name === name);
|
|
1332
|
+
if (directTool) return { server: direct, definition: directTool };
|
|
1333
|
+
for (const [, server] of scopedServerEntries(scopeId)) {
|
|
1334
|
+
const definition = server.tools?.find((tool) => tool.name === name);
|
|
1335
|
+
if (definition) return { server, definition };
|
|
1336
|
+
}
|
|
1337
|
+
return direct ? { server: direct, definition: null } : null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** Protocol tools under the `mcp__<server>__` prefix plus the feature tools.
|
|
1341
|
+
* Names that would exceed provider limits are shortened deterministically
|
|
1342
|
+
* (`providerSafeMcpToolName`); a tool whose name was shortened carries the
|
|
1343
|
+
* server's own tool name as `mcpToolName` and its server as `mcpServerName`.
|
|
1344
|
+
* A plugin-declared server's tools carry its owner as `mcpPluginId`, so
|
|
1345
|
+
* scope filtering never has to infer ownership from the server name. */
|
|
1346
|
+
function mcpServerTools(name, capabilities, rawTools, cfg) {
|
|
1347
|
+
const protocolTools = (rawTools || []).map((t) => {
|
|
1348
|
+
const safeName = providerSafeMcpToolName(name, t.name);
|
|
1349
|
+
return {
|
|
1350
|
+
name: safeName,
|
|
1351
|
+
...(safeName === `mcp__${name}__${t.name}` ? {} : { mcpToolName: t.name, mcpServerName: name }),
|
|
1352
|
+
description: t.description || '',
|
|
1353
|
+
inputSchema: t.inputSchema || { type: 'object', properties: {} },
|
|
1354
|
+
...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
|
|
1355
|
+
};
|
|
1356
|
+
});
|
|
1357
|
+
const tools = [...protocolTools, ...mcpFeatureTools(name, capabilities, protocolTools)];
|
|
1358
|
+
const pluginId = String(cfg?._mixdogPlugin?.id || '').trim();
|
|
1359
|
+
return pluginId ? tools.map((tool) => ({ ...tool, mcpPluginId: pluginId })) : tools;
|
|
1285
1360
|
}
|
|
1286
1361
|
|
|
1287
1362
|
// tools/list_changed refreshes run one at a time per server connection, so an
|
|
@@ -1304,7 +1379,7 @@ async function runToolRefresh(registryKey, live, name, cfg, capabilities) {
|
|
|
1304
1379
|
throw new Error('ListTools returned invalid shape: missing or non-array tools field');
|
|
1305
1380
|
}
|
|
1306
1381
|
if (servers.get(registryKey) !== live) return;
|
|
1307
|
-
live.tools = mcpServerTools(name, capabilities, refreshed.tools);
|
|
1382
|
+
live.tools = mcpServerTools(name, capabilities, refreshed.tools, cfg);
|
|
1308
1383
|
_invalidateMcpToolFieldMemo();
|
|
1309
1384
|
} catch (error) {
|
|
1310
1385
|
mcpLog(
|
|
@@ -1317,19 +1392,28 @@ async function runToolRefresh(registryKey, live, name, cfg, capabilities) {
|
|
|
1317
1392
|
export function _registerMcpServerForTest(scopeId, name, rawTools = [], options = {}) {
|
|
1318
1393
|
const normalizedScopeId = normalizeMcpScopeId(scopeId);
|
|
1319
1394
|
const registryKey = mcpServerRegistryKey(normalizedScopeId, name);
|
|
1320
|
-
const tools = rawTools.map((tool) =>
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1395
|
+
const tools = rawTools.map((tool) => {
|
|
1396
|
+
const prefixed = String(tool?.name || '').startsWith('mcp__');
|
|
1397
|
+
const leaf = String(tool?.name || '');
|
|
1398
|
+
const safeName = prefixed ? leaf : providerSafeMcpToolName(name, leaf);
|
|
1399
|
+
const shortened = !prefixed && safeName !== `mcp__${name}__${leaf}`;
|
|
1400
|
+
return {
|
|
1401
|
+
...tool,
|
|
1402
|
+
name: safeName,
|
|
1403
|
+
...(shortened ? { mcpToolName: leaf, mcpServerName: name } : {}),
|
|
1404
|
+
inputSchema: tool?.inputSchema || { type: 'object', properties: {} },
|
|
1405
|
+
};
|
|
1406
|
+
});
|
|
1327
1407
|
const capabilities = options.capabilities || {};
|
|
1408
|
+
const pluginId = String(options.cfg?._mixdogPlugin?.id || '').trim();
|
|
1328
1409
|
const server = {
|
|
1329
1410
|
scopeId: normalizedScopeId,
|
|
1330
1411
|
registryKey,
|
|
1331
1412
|
name,
|
|
1332
|
-
tools: [...tools, ...mcpFeatureTools(name, capabilities, tools)]
|
|
1413
|
+
tools: [...tools, ...mcpFeatureTools(name, capabilities, tools)].map((tool) =>
|
|
1414
|
+
pluginId ? { ...tool, mcpPluginId: pluginId } : tool
|
|
1415
|
+
),
|
|
1416
|
+
warnings: options.warnings || [],
|
|
1333
1417
|
cfg: options.cfg || {},
|
|
1334
1418
|
instructions: options.instructions || '',
|
|
1335
1419
|
capabilities,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The MCP SDK Client compiles every tool's outputSchema with Ajv inside
|
|
2
|
+
// listTools(), synchronously: a server listing ~180 tools held the daemon's
|
|
3
|
+
// event loop for 1-2 s on every connect. This validator compiles each schema
|
|
4
|
+
// on the tool's first structured result instead, and creates the SDK's Ajv
|
|
5
|
+
// validator (same options and error text) on first use too.
|
|
6
|
+
// A schema declaring an `$id` anywhere registers it in the client's Ajv, and a
|
|
7
|
+
// schema referencing anything outside itself resolves against what is
|
|
8
|
+
// registered by then (failing the listing when it is not); both still compile
|
|
9
|
+
// when listed, in listing order, exactly as the SDK does.
|
|
10
|
+
const LISTING_ORDER_SCHEMA = /"\$id"|"\$ref":"(?!#)/;
|
|
11
|
+
|
|
12
|
+
export function createLazyOutputValidator(AjvJsonSchemaValidator) {
|
|
13
|
+
let provider = null;
|
|
14
|
+
const shared = () => (provider ??= new AjvJsonSchemaValidator());
|
|
15
|
+
return {
|
|
16
|
+
getValidator(schema) {
|
|
17
|
+
if (LISTING_ORDER_SCHEMA.test(JSON.stringify(schema))) return shared().getValidator(schema);
|
|
18
|
+
let validate = null;
|
|
19
|
+
return (input) => {
|
|
20
|
+
validate ??= shared().getValidator(schema);
|
|
21
|
+
return validate(input);
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp/plugin-placeholders.mjs — plugin root/data placeholders in an MCP server
|
|
3
|
+
* config. Pure: no filesystem access; the caller supplies the directories.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ROOT_NAMES = ['CLAUDE_PLUGIN_ROOT', 'CODEX_PLUGIN_ROOT', 'PLUGIN_ROOT', 'MIXDOG_PLUGIN_ROOT'];
|
|
7
|
+
const DATA_NAMES = ['CLAUDE_PLUGIN_DATA', 'PLUGIN_DATA', 'MIXDOG_PLUGIN_DATA'];
|
|
8
|
+
const PLACEHOLDER_RE = new RegExp(`\\$\\{(${[...ROOT_NAMES, ...DATA_NAMES].join('|')})\\}`, 'g');
|
|
9
|
+
const CONFIG_FIELDS = ['command', 'args', 'env', 'url', 'headers', 'cwd'];
|
|
10
|
+
|
|
11
|
+
// Substituted paths use forward slashes on Windows so a path that lands in a
|
|
12
|
+
// command line or argument is not read as backslash escapes; Windows accepts
|
|
13
|
+
// them everywhere.
|
|
14
|
+
function normalizePath(path, platform) {
|
|
15
|
+
return platform === 'win32' ? path.replace(/\\/g, '/') : path;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function substitute(value, replacements) {
|
|
19
|
+
if (typeof value === 'string') {
|
|
20
|
+
// Function replacement: `$` sequences in paths are inserted literally.
|
|
21
|
+
return value.replace(PLACEHOLDER_RE, (match, name) => replacements.get(name) ?? match);
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) return value.map((v) => substitute(v, replacements));
|
|
24
|
+
if (value && typeof value === 'object') {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [k, v] of Object.entries(value)) out[k] = substitute(v, replacements);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns a copy of `config` with plugin root/data placeholders replaced in
|
|
34
|
+
* command, args, env, url, headers and cwd. A placeholder whose directory is
|
|
35
|
+
* not supplied is left as written. Run before ordinary env expansion.
|
|
36
|
+
*/
|
|
37
|
+
export function resolvePluginPlaceholders(config, { root, data, platform = process.platform } = {}) {
|
|
38
|
+
const replacements = new Map();
|
|
39
|
+
if (root) for (const name of ROOT_NAMES) replacements.set(name, normalizePath(String(root), platform));
|
|
40
|
+
if (data) for (const name of DATA_NAMES) replacements.set(name, normalizePath(String(data), platform));
|
|
41
|
+
const out = { ...config };
|
|
42
|
+
for (const field of CONFIG_FIELDS) {
|
|
43
|
+
if (out[field] !== undefined) out[field] = substitute(out[field], replacements);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Environment variables a plugin-owned server process receives (raw paths). */
|
|
49
|
+
export function pluginServerEnv({ root, data } = {}) {
|
|
50
|
+
const env = {};
|
|
51
|
+
if (root) for (const name of ['CLAUDE_PLUGIN_ROOT', 'MIXDOG_PLUGIN_ROOT', 'PLUGIN_ROOT']) env[name] = String(root);
|
|
52
|
+
if (data) for (const name of ['CLAUDE_PLUGIN_DATA', 'MIXDOG_PLUGIN_DATA', 'PLUGIN_DATA']) env[name] = String(data);
|
|
53
|
+
return env;
|
|
54
|
+
}
|