From c27e850e70339c43d71de2bac691af14fa13c118 Mon Sep 17 00:00:00 2001 From: jeonsworld Date: Thu, 17 Sep 2026 13:52:52 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Codex=EC=97=90=EC=84=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=8D=94=20MCP=EB=A5=BC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude-plugin/marketplace.json | 2 +- plugins/ennoia/.claude-plugin/plugin.json | 2 +- plugins/ennoia/.codex-plugin/plugin.json | 2 +- plugins/ennoia/.mcp.json | 3 ++- plugins/ennoia/mcp.json | 3 ++- plugins/ennoia/mcp/file-uploader.mjs | 3 ++- plugins/ennoia/plugin.json | 2 +- scripts/sync_manifests.py | 2 +- scripts/validate.py | 7 +++++-- tests/test_file_uploader.mjs | 1 + tests/test_file_uploader_lifecycle.mjs | 2 +- tests/test_validation.py | 14 ++++++++++++-- 12 files changed, 30 insertions(+), 13 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 68f7dd6..79485df 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ "name": "ennoia", "source": "./plugins/ennoia", "description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.", - "version": "1.4.1" + "version": "1.4.2" } ] } diff --git a/plugins/ennoia/.claude-plugin/plugin.json b/plugins/ennoia/.claude-plugin/plugin.json index 17ac2d4..fb89a8a 100644 --- a/plugins/ennoia/.claude-plugin/plugin.json +++ b/plugins/ennoia/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ennoia", - "version": "1.4.1", + "version": "1.4.2", "description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.", "author": { "name": "Wantedlab", diff --git a/plugins/ennoia/.codex-plugin/plugin.json b/plugins/ennoia/.codex-plugin/plugin.json index db7185c..1bc7274 100644 --- a/plugins/ennoia/.codex-plugin/plugin.json +++ b/plugins/ennoia/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "ennoia", - "version": "1.4.1", + "version": "1.4.2", "description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.", "author": { "name": "Wantedlab", diff --git a/plugins/ennoia/.mcp.json b/plugins/ennoia/.mcp.json index 8ed87df..24e17f7 100644 --- a/plugins/ennoia/.mcp.json +++ b/plugins/ennoia/.mcp.json @@ -5,8 +5,9 @@ "url": "https://mcp.ennoia.so/mcp" }, "ennoia-file-uploader": { + "type": "stdio", "command": "node", - "cwd": ".", + "cwd": "./", "args": [ "-e", "import(require('node:url').pathToFileURL(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT || process.cwd(), 'mcp/file-uploader.mjs')).href).then(m => m.serve()).catch(() => { process.exitCode = 1; })" diff --git a/plugins/ennoia/mcp.json b/plugins/ennoia/mcp.json index 723dd72..05a7411 100644 --- a/plugins/ennoia/mcp.json +++ b/plugins/ennoia/mcp.json @@ -6,8 +6,9 @@ "url": "https://mcp.ennoia.so/mcp" }, "ennoia-file-uploader": { + "type": "stdio", "command": "node", - "cwd": ".", + "cwd": "./", "args": [ "-e", "import(require('node:url').pathToFileURL(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT || process.cwd(), 'mcp/file-uploader.mjs')).href).then(m => m.serve()).catch(() => { process.exitCode = 1; })" diff --git a/plugins/ennoia/mcp/file-uploader.mjs b/plugins/ennoia/mcp/file-uploader.mjs index 71e862f..0e38b71 100644 --- a/plugins/ennoia/mcp/file-uploader.mjs +++ b/plugins/ennoia/mcp/file-uploader.mjs @@ -15,6 +15,7 @@ const UPLOAD_TIMEOUT_MS = 120000; const EXTENSIONS = new Set(['.csv', '.txt', '.md', '.pdf', '.docx', '.pptx', '.xlsx', '.xls', '.zip']); const HOSTS = new Set(['mcp.ennoia.so', 'dev-mcp-server.ennoia.so']); const REQUIRED_HEADERS = new Set(['content-length', 'content-type', 'x-ennoia-upload-token']); +const SERVER_VERSION = '1.4.2'; const ERROR_DETAILS = Object.freeze({ ARGUMENTS_INVALID: ['업로드 인자 형식이 올바르지 않습니다.', 'local_path, upload_url, headers만 전달하세요.'], UPLOAD_URL_INVALID: ['업로드 URL을 사용할 수 없습니다.', 'prepare_rag_document_upload로 새 upload ticket을 발급하세요.'], @@ -199,7 +200,7 @@ async function handleRpc(message, upload = uploadFile) { const reply = result => ({ jsonrpc: '2.0', id, result }); if (message.method === 'initialize') return reply({ protocolVersion: ['2024-11-05', '2025-03-26', '2025-06-18'].includes(message.params?.protocolVersion) ? message.params.protocolVersion : '2025-06-18', - capabilities: { tools: {} }, serverInfo: { name: 'ennoia-file-uploader', version: '1.4.1' }, + capabilities: { tools: {} }, serverInfo: { name: 'ennoia-file-uploader', version: SERVER_VERSION }, }); if (message.method === 'ping') return reply({}); if (message.method === 'tools/list') return reply({ tools: [uploadTool] }); diff --git a/plugins/ennoia/plugin.json b/plugins/ennoia/plugin.json index cccac3a..7a83644 100644 --- a/plugins/ennoia/plugin.json +++ b/plugins/ennoia/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "ennoia", - "version": "1.4.1", + "version": "1.4.2", "description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.", "author": { "name": "Wantedlab", diff --git a/scripts/sync_manifests.py b/scripts/sync_manifests.py index 10b2144..ebea549 100644 --- a/scripts/sync_manifests.py +++ b/scripts/sync_manifests.py @@ -15,7 +15,7 @@ def outputs(root: Path) -> dict[Path, dict]: identity = {k: v for k, v in manifest.items() if k not in {"$schema", "extensions"}} expected_servers = { "ennoia": {"type": "streamable-http", "url": "https://mcp.ennoia.so/mcp"}, - "ennoia-file-uploader": {"command": "node", "cwd": ".", "args": ["-e", "import(require('node:url').pathToFileURL(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT || process.cwd(), 'mcp/file-uploader.mjs')).href).then(m => m.serve()).catch(() => { process.exitCode = 1; })"]}, + "ennoia-file-uploader": {"type": "stdio", "command": "node", "cwd": "./", "args": ["-e", "import(require('node:url').pathToFileURL(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT || process.cwd(), 'mcp/file-uploader.mjs')).href).then(m => m.serve()).catch(() => { process.exitCode = 1; })"]}, } if mcp.get("mcpServers") != expected_servers: raise ValueError("Ennoia 배포는 지정한 remote MCP와 local file uploader만 지원합니다.") diff --git a/scripts/validate.py b/scripts/validate.py index e04c107..8aade5a 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -59,13 +59,16 @@ def validate_repository(root: Path) -> list[str]: mcp = read_json(plugin / filename) expected_servers = { "ennoia": {"type": transport, "url": "https://mcp.ennoia.so/mcp"}, - "ennoia-file-uploader": {"command": "node", "cwd": ".", "args": ["-e", "import(require('node:url').pathToFileURL(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT || process.cwd(), 'mcp/file-uploader.mjs')).href).then(m => m.serve()).catch(() => { process.exitCode = 1; })"]}, + "ennoia-file-uploader": {"type": "stdio", "command": "node", "cwd": "./", "args": ["-e", "import(require('node:url').pathToFileURL(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT || process.cwd(), 'mcp/file-uploader.mjs')).href).then(m => m.serve()).catch(() => { process.exitCode = 1; })"]}, } if mcp.get("mcpServers") != expected_servers: errors.append(f"MCP 설정 오류 또는 credential 포함: {filename}") - if not (plugin / "mcp/file-uploader.mjs").is_file(): + uploader = plugin / "mcp/file-uploader.mjs" + if not uploader.is_file(): errors.append("MCP local file uploader 누락") + elif f"const SERVER_VERSION = '{portable['version']}';" not in uploader.read_text(encoding="utf-8"): + errors.append("MCP local file uploader version 불일치") a = read_json(root / ".agents/plugins/marketplace.json") b = read_json(root / ".claude-plugin/marketplace.json") diff --git a/tests/test_file_uploader.mjs b/tests/test_file_uploader.mjs index 7d41467..27cb293 100644 --- a/tests/test_file_uploader.mjs +++ b/tests/test_file_uploader.mjs @@ -62,6 +62,7 @@ test('stdio initializes, ignores notification, lists exact tool and returns safe assert.equal(replies.length, 4); assert.equal(replies[0].result.protocolVersion, '2025-03-26'); assert.deepEqual(replies[0].result.capabilities, { tools: {} }); + assert.deepEqual(replies[0].result.serverInfo, { name: 'ennoia-file-uploader', version: '1.4.2' }); const [tool] = replies[1].result.tools; assert.equal(tool.name, 'upload_ennoia_rag_file'); assert.deepEqual(tool.inputSchema.required, ['local_path', 'upload_url', 'headers']); diff --git a/tests/test_file_uploader_lifecycle.mjs b/tests/test_file_uploader_lifecycle.mjs index 990de2b..fbb20d3 100644 --- a/tests/test_file_uploader_lifecycle.mjs +++ b/tests/test_file_uploader_lifecycle.mjs @@ -42,7 +42,7 @@ for (const host of ['claude', 'codex']) test(`generated launch resolves ${host} await mkdir(join(dir, 'mcp')); await writeFile(join(dir, 'mcp/file-uploader.mjs'), 'process.stdout.write("WRONG_PROJECT_SCRIPT");'); const config = JSON.parse(await readFile(join(plugin, '.mcp.json'))).mcpServers['ennoia-file-uploader']; - assert.equal(config.command, 'node'); assert.equal(config.cwd, '.'); + assert.equal(config.type, 'stdio'); assert.equal(config.command, 'node'); assert.equal(config.cwd, './'); const env = { ...process.env }; delete env.CLAUDE_PLUGIN_ROOT; if (host === 'claude') env.CLAUDE_PLUGIN_ROOT = plugin; diff --git a/tests/test_validation.py b/tests/test_validation.py index c7a61a7..96a7f2b 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -32,14 +32,19 @@ def test_local_uploader_is_packaged_and_contract_changes_are_rejected(self): path = plugin / filename original = json.loads(path.read_text()) self.assertEqual(original["mcpServers"]["ennoia-file-uploader"], { - "command": "node", "cwd": ".", "args": ["-e", "import(require('node:url').pathToFileURL(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT || process.cwd(), 'mcp/file-uploader.mjs')).href).then(m => m.serve()).catch(() => { process.exitCode = 1; })"], + "type": "stdio", "command": "node", "cwd": "./", "args": ["-e", "import(require('node:url').pathToFileURL(require('node:path').join(process.env.CLAUDE_PLUGIN_ROOT || process.cwd(), 'mcp/file-uploader.mjs')).href).then(m => m.serve()).catch(() => { process.exitCode = 1; })"], }) - for key, value in (("command", "sh"), ("cwd", ".."), ("args", ["./other.mjs"]), ("env", {"TOKEN": "secret"})): + for key, value in (("type", "http"), ("command", "sh"), ("cwd", "."), ("args", ["./other.mjs"]), ("env", {"TOKEN": "secret"})): data = json.loads(json.dumps(original)) data["mcpServers"]["ennoia-file-uploader"][key] = value path.write_text(json.dumps(data)) self.assertTrue(any("MCP" in e for e in self.validate(self.root))) path.write_text(json.dumps(original)) + data = json.loads(json.dumps(original)) + del data["mcpServers"]["ennoia-file-uploader"]["type"] + path.write_text(json.dumps(data)) + self.assertTrue(any("MCP" in e for e in self.validate(self.root))) + path.write_text(json.dumps(original)) (plugin / "mcp/file-uploader.mjs").unlink() self.assertTrue(any("MCP" in e for e in self.validate(self.root))) @@ -95,6 +100,11 @@ def test_version_drift_is_rejected(self): path.write_text(json.dumps(data)) self.assertTrue(any("manifest" in e for e in self.validate(self.root))) + def test_local_uploader_version_drift_is_rejected(self): + path = self.root / "plugins/ennoia/mcp/file-uploader.mjs" + path.write_text(path.read_text().replace("SERVER_VERSION = '1.4.2'", "SERVER_VERSION = '0.0.0'")) + self.assertTrue(any("uploader version" in e for e in self.validate(self.root))) + def test_hallucinated_tool_reference_is_rejected(self): skill = self.root / "plugins/ennoia/skills/ennoia-run/SKILL.md" skill.write_text(skill.read_text() + "\n`get_ennoia_fictional_resume`\n")