completed tools and setup
This commit is contained in:
103
EXTERNAL_WP_CLI_TESTING_SETUP.md
Normal file
103
EXTERNAL_WP_CLI_TESTING_SETUP.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# External WP-CLI Testing Setup
|
||||||
|
|
||||||
|
This guide explains how to enable and run External WP-CLI Testing from the Builder, with tools loaded into OpenCode only when the feature toggle is enabled.
|
||||||
|
|
||||||
|
## What This Adds
|
||||||
|
|
||||||
|
- A gated MCP tool named `test_plugin_external_wp` (plus a helper tool `external_wp_testing_config`).
|
||||||
|
- Tools are injected into OpenCode only when the Builder toggle is on.
|
||||||
|
- All connection/auth settings are controlled via environment variables.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. A reachable WordPress host with WP-CLI installed.
|
||||||
|
2. SSH access to that host from the server running the chat app.
|
||||||
|
3. The SSH user must be able to run WP-CLI at the configured path.
|
||||||
|
|
||||||
|
## Required Environment Variables
|
||||||
|
|
||||||
|
Set these in the environment where the chat server runs (the same process that launches OpenCode).
|
||||||
|
|
||||||
|
- `TEST_WP_HOST` (or `EXTERNAL_WP_HOST`)
|
||||||
|
- Hostname or IP of the WordPress server.
|
||||||
|
- `TEST_WP_SSH_USER` (or `EXTERNAL_WP_SSH_USER`)
|
||||||
|
- SSH username for the WordPress server.
|
||||||
|
- `TEST_WP_SSH_KEY` (or `TEST_WP_SSH_KEY_PATH`, or `EXTERNAL_WP_SSH_KEY`)
|
||||||
|
- Path to the SSH private key file.
|
||||||
|
|
||||||
|
If these are missing, the tool will return a clear error.
|
||||||
|
|
||||||
|
## Optional Environment Variables
|
||||||
|
|
||||||
|
- `TEST_WP_PATH` (or `EXTERNAL_WP_PATH`)
|
||||||
|
- Default: `/var/www/html`
|
||||||
|
- Path to the WordPress root on the remote host.
|
||||||
|
- `TEST_WP_BASE_URL` (or `EXTERNAL_WP_BASE_URL`)
|
||||||
|
- Default: `https://<TEST_WP_HOST>`
|
||||||
|
- Base URL for the site.
|
||||||
|
- `TEST_WP_MULTISITE` (or `EXTERNAL_WP_MULTISITE`)
|
||||||
|
- Default: `true`
|
||||||
|
- Enables multisite subsite isolation.
|
||||||
|
- `TEST_WP_SUBSITE_PREFIX` (or `EXTERNAL_WP_SUBSITE_PREFIX`)
|
||||||
|
- Default: `test`
|
||||||
|
- Prefix for created subsites.
|
||||||
|
- `TEST_WP_SUBSITE_DOMAIN` (or `EXTERNAL_WP_SUBSITE_DOMAIN`)
|
||||||
|
- Default: empty (falls back to host)
|
||||||
|
- Domain used when subsite mode is subdomain.
|
||||||
|
- `TEST_WP_SUBSITE_MODE` (or `EXTERNAL_WP_SUBSITE_MODE`)
|
||||||
|
- Default: `subdirectory`
|
||||||
|
- Allowed values: `subdirectory`, `subdomain`.
|
||||||
|
- `TEST_WP_ERROR_LOG` (or `EXTERNAL_WP_ERROR_LOG`)
|
||||||
|
- Default: `/var/log/wp-errors.log`
|
||||||
|
- Used by the default "Scan error log" scenario.
|
||||||
|
- `TEST_SSH_STRICT` (or `EXTERNAL_WP_SSH_STRICT`)
|
||||||
|
- Default: `false`
|
||||||
|
- When `true`, enforces strict host key checking.
|
||||||
|
- `TEST_MAX_CONCURRENT` (or `EXTERNAL_WP_MAX_CONCURRENT`)
|
||||||
|
- Default: `20`
|
||||||
|
- Max concurrent tests.
|
||||||
|
- `TEST_QUEUE_TIMEOUT` (or `EXTERNAL_WP_QUEUE_TIMEOUT`)
|
||||||
|
- Default: `300000` (ms)
|
||||||
|
- `TEST_TIMEOUT` (or `EXTERNAL_WP_TEST_TIMEOUT`)
|
||||||
|
- Default: `600000` (ms)
|
||||||
|
- `TEST_AUTO_CLEANUP` (or `EXTERNAL_WP_AUTO_CLEANUP`)
|
||||||
|
- Default: `true`
|
||||||
|
- Cleans up subsites after tests complete.
|
||||||
|
- `TEST_CLEANUP_DELAY` (or `EXTERNAL_WP_CLEANUP_DELAY`)
|
||||||
|
- Default: `3600000` (ms)
|
||||||
|
|
||||||
|
## Example .env
|
||||||
|
|
||||||
|
TEST_WP_HOST=wp-test.example.com
|
||||||
|
TEST_WP_SSH_USER=wordpress
|
||||||
|
TEST_WP_SSH_KEY=C:\\keys\\wp-test.pem
|
||||||
|
TEST_WP_PATH=/var/www/html
|
||||||
|
TEST_WP_BASE_URL=https://wp-test.example.com
|
||||||
|
TEST_WP_MULTISITE=true
|
||||||
|
TEST_WP_SUBSITE_MODE=subdirectory
|
||||||
|
TEST_WP_SUBSITE_PREFIX=test
|
||||||
|
TEST_WP_ERROR_LOG=/var/log/wp-errors.log
|
||||||
|
TEST_SSH_STRICT=false
|
||||||
|
TEST_MAX_CONCURRENT=20
|
||||||
|
TEST_TIMEOUT=600000
|
||||||
|
|
||||||
|
## Builder Toggle Behavior
|
||||||
|
|
||||||
|
- Open the Builder and enable the "External WP CLI testing" toggle.
|
||||||
|
- When enabled, the chat server injects the MCP server definition via `OPENCODE_EXTRA_MCP_SERVERS`.
|
||||||
|
- When disabled, the environment variable is removed before OpenCode is spawned, so the tools are not exposed.
|
||||||
|
|
||||||
|
## Running a Test
|
||||||
|
|
||||||
|
1. Enable the toggle in Builder.
|
||||||
|
2. Proceed with the build or let the AI call `test_plugin_external_wp`.
|
||||||
|
3. The tool runs WP-CLI commands over SSH and returns a JSON report.
|
||||||
|
|
||||||
|
## Quick Sanity Check Tool
|
||||||
|
|
||||||
|
You can ask the AI to call `external_wp_testing_config` to validate your settings. It returns the resolved config and a list of missing required variables.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `test_plugin_external_wp` only supports `test_mode=cli` in the current implementation.
|
||||||
|
- Ensure the SSH user can execute WP-CLI and write to the plugin directory.
|
||||||
@@ -3964,6 +3964,7 @@ async function sendMessage() {
|
|||||||
model,
|
model,
|
||||||
cli,
|
cli,
|
||||||
attachments: pendingAttachments.length ? pendingAttachments : undefined,
|
attachments: pendingAttachments.length ? pendingAttachments : undefined,
|
||||||
|
externalTestingEnabled: !!builderState.externalTestingEnabled // Send feature toggle state to server
|
||||||
};
|
};
|
||||||
// Preserve opencodeSessionId to continue in the same session
|
// Preserve opencodeSessionId to continue in the same session
|
||||||
if (currentSession && currentSession.opencodeSessionId) {
|
if (currentSession && currentSession.opencodeSessionId) {
|
||||||
|
|||||||
@@ -8800,6 +8800,14 @@ async function sendToOpencode({ session, model, content, message, cli, streamCal
|
|||||||
args.push('--session', opencodeSessionId);
|
args.push('--session', opencodeSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure WP CLI Testing MCP server if feature is enabled
|
||||||
|
const wpCliMcpEnabled = message?.externalTestingEnabled === true;
|
||||||
|
log('WP CLI Testing MCP server configuration', {
|
||||||
|
enabled: wpCliMcpEnabled,
|
||||||
|
messageId: message?.id,
|
||||||
|
sessionId: session.id
|
||||||
|
});
|
||||||
|
|
||||||
// Ensure content is properly passed as the final argument
|
// Ensure content is properly passed as the final argument
|
||||||
if (typeof clean !== 'string' || clean.length === 0) {
|
if (typeof clean !== 'string' || clean.length === 0) {
|
||||||
throw new Error('Message content is invalid or empty after sanitization');
|
throw new Error('Message content is invalid or empty after sanitization');
|
||||||
@@ -8866,6 +8874,36 @@ async function sendToOpencode({ session, model, content, message, cli, streamCal
|
|||||||
|
|
||||||
// Use the OpenCode process manager for execution
|
// Use the OpenCode process manager for execution
|
||||||
// This ensures all sessions share the same OpenCode instance when possible
|
// This ensures all sessions share the same OpenCode instance when possible
|
||||||
|
|
||||||
|
// Build environment variables including WP CLI MCP server config
|
||||||
|
const executionEnv = {
|
||||||
|
...process.env,
|
||||||
|
OPENAI_API_KEY: OPENCODE_OLLAMA_API_KEY
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add WP CLI Testing MCP server to OpenCode config if enabled
|
||||||
|
if (wpCliMcpEnabled) {
|
||||||
|
const wpMcpServerPath = path.resolve(__dirname, '../opencode/mcp-servers/wp-cli-testing/index.js');
|
||||||
|
log('Enabling WP CLI Testing MCP server', { path: wpMcpServerPath, messageId: message?.id });
|
||||||
|
|
||||||
|
// Configure OpenCode to include the WP CLI testing MCP server
|
||||||
|
// We pass this via environment variable that OpenCode can read
|
||||||
|
executionEnv.OPENCODE_EXTRA_MCP_SERVERS = JSON.stringify([
|
||||||
|
{
|
||||||
|
name: 'wp-cli-testing',
|
||||||
|
command: 'node',
|
||||||
|
args: [wpMcpServerPath],
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// Safety: ensure the tools are not injected when the builder toggle is off,
|
||||||
|
// even if the parent process environment happens to have the variable set.
|
||||||
|
if (executionEnv.OPENCODE_EXTRA_MCP_SERVERS) {
|
||||||
|
delete executionEnv.OPENCODE_EXTRA_MCP_SERVERS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { stdout, stderr } = await opencodeManager.executeInSession(
|
const { stdout, stderr } = await opencodeManager.executeInSession(
|
||||||
session?.id || 'standalone',
|
session?.id || 'standalone',
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
@@ -8873,10 +8911,7 @@ async function sendToOpencode({ session, model, content, message, cli, streamCal
|
|||||||
args,
|
args,
|
||||||
{
|
{
|
||||||
timeout: 600000, // 10 minute timeout to prevent stuck processes
|
timeout: 600000, // 10 minute timeout to prevent stuck processes
|
||||||
env: {
|
env: executionEnv,
|
||||||
...process.env,
|
|
||||||
OPENAI_API_KEY: OPENCODE_OLLAMA_API_KEY
|
|
||||||
},
|
|
||||||
onData: (type, chunk) => {
|
onData: (type, chunk) => {
|
||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|||||||
274
opencode/mcp-servers/wp-cli-testing/index.js
Normal file
274
opencode/mcp-servers/wp-cli-testing/index.js
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
// This MCP server wraps the existing external WP testing implementation used by the chat app.
|
||||||
|
// It is intentionally kept small and env-configurable so host apps can load it only when needed.
|
||||||
|
|
||||||
|
const ToolInput = {
|
||||||
|
plugin_path: z.string().min(1),
|
||||||
|
plugin_slug: z.string().optional(),
|
||||||
|
session_id: z.string().optional(),
|
||||||
|
test_mode: z.enum(["cli", "visual", "both"]).optional().default("cli"),
|
||||||
|
required_plugins: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
plugin_slug: z.string().min(1),
|
||||||
|
source: z.enum(["wordpress.org", "url", "local"]).optional(),
|
||||||
|
source_url: z.string().optional(),
|
||||||
|
activate: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
test_scenarios: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
|
url: z.string().optional(),
|
||||||
|
selector: z.string().optional(),
|
||||||
|
expected_text: z.string().optional(),
|
||||||
|
wp_cli_command: z.string().optional(),
|
||||||
|
shortcode: z.string().optional(),
|
||||||
|
hook: z.string().optional(),
|
||||||
|
ajax_action: z.string().optional(),
|
||||||
|
method: z.string().optional(),
|
||||||
|
body: z.string().optional(),
|
||||||
|
assertions: z
|
||||||
|
.object({
|
||||||
|
status_code: z.number().optional(),
|
||||||
|
contains: z.array(z.string()).optional(),
|
||||||
|
not_contains: z.array(z.string()).optional(),
|
||||||
|
wp_cli_success: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
config_overrides: z.record(z.any()).optional(),
|
||||||
|
}
|
||||||
|
|
||||||
|
function toJsonSchema(schema) {
|
||||||
|
// zod v4 exposes toJSONSchema under z.* in OpenCode, but in plain node usage
|
||||||
|
// we keep schema definitions explicit below for MCP.
|
||||||
|
// This helper exists only for future extension.
|
||||||
|
void schema
|
||||||
|
return {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestToolInputJsonSchema = {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["plugin_path"],
|
||||||
|
properties: {
|
||||||
|
plugin_path: { type: "string", description: "Local filesystem path to plugin root" },
|
||||||
|
plugin_slug: { type: "string", description: "Plugin slug (defaults to folder name)" },
|
||||||
|
session_id: { type: "string", description: "Optional session id for isolation" },
|
||||||
|
test_mode: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["cli", "visual", "both"],
|
||||||
|
description: "Testing mode (only 'cli' is implemented in this server)",
|
||||||
|
},
|
||||||
|
required_plugins: {
|
||||||
|
type: "array",
|
||||||
|
description: "Plugins to install/activate before running scenarios",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ["plugin_slug"],
|
||||||
|
properties: {
|
||||||
|
plugin_slug: { type: "string" },
|
||||||
|
source: { type: "string", enum: ["wordpress.org", "url", "local"] },
|
||||||
|
source_url: { type: "string" },
|
||||||
|
activate: { type: "boolean" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test_scenarios: {
|
||||||
|
type: "array",
|
||||||
|
description: "Scenario list; if omitted, defaults are generated",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
config_overrides: {
|
||||||
|
type: "object",
|
||||||
|
description: "Overrides for external WP testing configuration (see docs/env vars)",
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: "wp-cli-testing",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return {
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
name: "test_plugin_external_wp",
|
||||||
|
description:
|
||||||
|
"Run WP-CLI based verification against an externally hosted WordPress test site (over SSH).",
|
||||||
|
inputSchema: TestToolInputJsonSchema,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "external_wp_testing_config",
|
||||||
|
description:
|
||||||
|
"Return the resolved external WP testing config (from env vars) and highlight missing/invalid settings.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: false,
|
||||||
|
properties: {
|
||||||
|
config_overrides: {
|
||||||
|
type: "object",
|
||||||
|
additionalProperties: true,
|
||||||
|
description: "Optional config overrides merged on top of env defaults",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
||||||
|
const toolName = req.params.name
|
||||||
|
const args = (req.params.arguments ?? {})
|
||||||
|
|
||||||
|
// Lazy-load to avoid paying cost unless tool is actually invoked.
|
||||||
|
// Note: external-wp-testing.js is CommonJS; import default gives module.exports.
|
||||||
|
const externalTesting = await import("../../../chat/external-wp-testing.js")
|
||||||
|
const mod = externalTesting.default ?? externalTesting
|
||||||
|
const createExternalWpTester = mod?.createExternalWpTester
|
||||||
|
const getExternalTestingConfig = mod?.getExternalTestingConfig
|
||||||
|
|
||||||
|
if (typeof createExternalWpTester !== "function" || typeof getExternalTestingConfig !== "function") {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text:
|
||||||
|
"wp-cli-testing MCP server misconfigured: could not load chat/external-wp-testing.js exports.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolName === "external_wp_testing_config") {
|
||||||
|
const overrides =
|
||||||
|
args && typeof args === "object" && (args.config_overrides && typeof args.config_overrides === "object")
|
||||||
|
? args.config_overrides
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const config = getExternalTestingConfig(overrides)
|
||||||
|
const missing = []
|
||||||
|
if (!config.wpHost) missing.push("TEST_WP_HOST (or EXTERNAL_WP_HOST)")
|
||||||
|
if (!config.wpSshUser) missing.push("TEST_WP_SSH_USER (or EXTERNAL_WP_SSH_USER)")
|
||||||
|
if (!config.wpSshKey) missing.push("TEST_WP_SSH_KEY (or EXTERNAL_WP_SSH_KEY)")
|
||||||
|
|
||||||
|
const safeConfig = {
|
||||||
|
...config,
|
||||||
|
// avoid leaking filesystem layout too much; keep key path but don’t include any private key contents.
|
||||||
|
wpSshKey: config.wpSshKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
ok: missing.length === 0,
|
||||||
|
missing,
|
||||||
|
config: safeConfig,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolName === "test_plugin_external_wp") {
|
||||||
|
const parsed = z
|
||||||
|
.object(ToolInput)
|
||||||
|
.safeParse(args && typeof args === "object" ? args : {})
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify({ ok: false, error: parsed.error.message }, null, 2) }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.data.test_mode !== "cli") {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(
|
||||||
|
{
|
||||||
|
ok: false,
|
||||||
|
error:
|
||||||
|
"Only test_mode='cli' is implemented by this MCP server. Use cli mode or extend the server.",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tester = createExternalWpTester({})
|
||||||
|
|
||||||
|
const result = await tester.runTest(
|
||||||
|
{
|
||||||
|
plugin_path: parsed.data.plugin_path,
|
||||||
|
plugin_slug: parsed.data.plugin_slug,
|
||||||
|
session_id: parsed.data.session_id,
|
||||||
|
required_plugins: parsed.data.required_plugins ?? [],
|
||||||
|
test_scenarios: parsed.data.test_scenarios ?? [],
|
||||||
|
test_mode: "cli",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
configOverrides:
|
||||||
|
parsed.data.config_overrides && typeof parsed.data.config_overrides === "object"
|
||||||
|
? parsed.data.config_overrides
|
||||||
|
: {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
|
||||||
|
isError: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const transport = new StdioServerTransport()
|
||||||
|
await server.connect(transport)
|
||||||
@@ -235,6 +235,69 @@ export namespace Config {
|
|||||||
result.compaction = { ...result.compaction, prune: false }
|
result.compaction = { ...result.compaction, prune: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow host applications to inject additional MCP servers at runtime.
|
||||||
|
// This is intentionally env-driven so tools can be loaded only when needed
|
||||||
|
// (e.g. a Builder feature toggle) without requiring writing config files.
|
||||||
|
const extraMcpRaw = process.env.OPENCODE_EXTRA_MCP_SERVERS
|
||||||
|
if (extraMcpRaw && typeof extraMcpRaw === "string") {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(extraMcpRaw)
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
result.mcp ??= {}
|
||||||
|
for (const entry of parsed) {
|
||||||
|
if (!entry || typeof entry !== "object") continue
|
||||||
|
const name = typeof (entry as any).name === "string" ? (entry as any).name.trim() : ""
|
||||||
|
if (!name) continue
|
||||||
|
|
||||||
|
const enabledFromEntry = (entry as any).enabled
|
||||||
|
const disabledFromEntry = (entry as any).disabled
|
||||||
|
const enabled =
|
||||||
|
typeof enabledFromEntry === "boolean"
|
||||||
|
? enabledFromEntry
|
||||||
|
: typeof disabledFromEntry === "boolean"
|
||||||
|
? !disabledFromEntry
|
||||||
|
: true
|
||||||
|
|
||||||
|
const env = (entry as any).env
|
||||||
|
const envRecord: Record<string, string> | undefined =
|
||||||
|
env && typeof env === "object" && !Array.isArray(env) ? (env as Record<string, string>) : undefined
|
||||||
|
|
||||||
|
const timeout = (entry as any).timeout
|
||||||
|
const timeoutMs = Number.isFinite(timeout) ? Math.max(0, Number(timeout)) : undefined
|
||||||
|
|
||||||
|
// Accept either { command: 'node', args: [...] } or { command: ['node', ...] }
|
||||||
|
const commandVal = (entry as any).command
|
||||||
|
const argsVal = (entry as any).args
|
||||||
|
|
||||||
|
const commandParts: string[] = Array.isArray(commandVal)
|
||||||
|
? commandVal.filter((x: any) => typeof x === "string" && x.length)
|
||||||
|
: typeof commandVal === "string" && commandVal.trim().length
|
||||||
|
? [commandVal.trim()]
|
||||||
|
: []
|
||||||
|
|
||||||
|
const argsParts: string[] = Array.isArray(argsVal) ? argsVal.filter((x: any) => typeof x === "string") : []
|
||||||
|
|
||||||
|
if (commandParts.length === 0) {
|
||||||
|
log.warn("Ignoring OPENCODE_EXTRA_MCP_SERVERS entry without command", { name })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.mcp[name] = {
|
||||||
|
type: "local",
|
||||||
|
command: [...commandParts, ...argsParts],
|
||||||
|
env: envRecord,
|
||||||
|
enabled,
|
||||||
|
...(timeoutMs !== undefined ? { timeout: timeoutMs } : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("OPENCODE_EXTRA_MCP_SERVERS must be a JSON array; ignoring", { valueType: typeof parsed })
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.warn("Failed to parse OPENCODE_EXTRA_MCP_SERVERS; ignoring", { error: err instanceof Error ? err.message : String(err) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result.plugin = deduplicatePlugins(result.plugin ?? [])
|
result.plugin = deduplicatePlugins(result.plugin ?? [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user