Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"name": "ennoia",
"source": "./plugins/ennoia",
"description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.",
"version": "1.4.1"
"version": "1.4.2"
}
]
}
2 changes: 1 addition & 1 deletion plugins/ennoia/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ennoia",
"version": "1.4.1",
"version": "1.4.2",
"description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.",
"author": {
"name": "Wantedlab",
Expand Down
2 changes: 1 addition & 1 deletion plugins/ennoia/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ennoia",
"version": "1.4.1",
"version": "1.4.2",
"description": "Ennoia에서 에이전트 생성, 문서 지식 연결, App 실행, 운영 진단과 제품 피드백을 수행합니다.",
"author": {
"name": "Wantedlab",
Expand Down
3 changes: 2 additions & 1 deletion plugins/ennoia/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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; })"
Expand Down
3 changes: 2 additions & 1 deletion plugins/ennoia/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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; })"
Expand Down
3 changes: 2 additions & 1 deletion plugins/ennoia/mcp/file-uploader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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을 발급하세요.'],
Expand Down Expand Up @@ -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] });
Expand Down
2 changes: 1 addition & 1 deletion plugins/ennoia/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion scripts/sync_manifests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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만 지원합니다.")
Expand Down
7 changes: 5 additions & 2 deletions scripts/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions tests/test_file_uploader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
Expand Down
2 changes: 1 addition & 1 deletion tests/test_file_uploader_lifecycle.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 12 additions & 2 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down Expand Up @@ -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")
Expand Down
Loading