phalanx-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +47 -0
- package/dist/index.js +11 -0
- package/dist/tools/get-naming-rules.js +8 -0
- package/dist/tools/index.js +12 -0
- package/dist/tools/list-bundle-files.js +8 -0
- package/dist/tools/list-bundles.js +20 -0
- package/dist/tools/list-projects.js +8 -0
- package/dist/tools/list-recent-files.js +13 -0
- package/dist/tools/shared.js +19 -0
- package/package.json +23 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// 생성자 parameter property는 쓰지 않는다. node --test가 strip-only 모드로 .ts를 실행하는데
|
|
2
|
+
// 그 문법은 변환이 필요해 거절된다.
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
constructor(status, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ApiError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const ENV_HELP = "PHALANX_URL과 PHALANX_API_KEY 환경변수가 필요합니다. 툴의 /api-keys 화면에서 키를 발급하면 MCP 설정 JSON이 함께 나옵니다.";
|
|
12
|
+
// 서버 시작이 아니라 도구를 부를 때 읽는다. 시작 자체가 죽으면 호스트는 이유를 보여 주지 않는다.
|
|
13
|
+
export function readEnv(env) {
|
|
14
|
+
const url = env.PHALANX_URL?.replace(/\/+$/, "");
|
|
15
|
+
const key = env.PHALANX_API_KEY;
|
|
16
|
+
if (!url) {
|
|
17
|
+
throw new Error(`PHALANX_URL이 없습니다. ${ENV_HELP}`);
|
|
18
|
+
}
|
|
19
|
+
if (!key) {
|
|
20
|
+
throw new Error(`PHALANX_API_KEY가 없습니다. ${ENV_HELP}`);
|
|
21
|
+
}
|
|
22
|
+
return { url, key };
|
|
23
|
+
}
|
|
24
|
+
// 사용자가 따라갈 수 있는 문구로 바꾼다. 401은 키 문제라 발급 화면을 가리킨다.
|
|
25
|
+
export function describeApiError(status, serverMessage, serverUrl) {
|
|
26
|
+
const base = serverMessage ?? `서버가 ${status}를 돌려주었습니다.`;
|
|
27
|
+
if (status === 401) {
|
|
28
|
+
return `API 키가 없거나 폐기되었습니다. ${serverUrl}/api-keys 에서 새로 발급해 PHALANX_API_KEY에 넣으세요. (${base})`;
|
|
29
|
+
}
|
|
30
|
+
return base;
|
|
31
|
+
}
|
|
32
|
+
export function createClient(config, fetchFn = fetch) {
|
|
33
|
+
return {
|
|
34
|
+
async get(path, query = {}) {
|
|
35
|
+
const params = new URLSearchParams(Object.entries(query).filter((entry) => entry[1] !== undefined));
|
|
36
|
+
const search = params.size > 0 ? `?${params}` : "";
|
|
37
|
+
const response = await fetchFn(`${config.url}${path}${search}`, {
|
|
38
|
+
headers: { "x-api-key": config.key, accept: "application/json" },
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const body = (await response.json().catch(() => null));
|
|
42
|
+
throw new ApiError(response.status, describeApiError(response.status, body?.error ?? null, config.url));
|
|
43
|
+
}
|
|
44
|
+
return response.json();
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
4
|
+
import { createClient, readEnv } from "./client.js";
|
|
5
|
+
import { registerTools } from "./tools/index.js";
|
|
6
|
+
// env 검증은 도구를 부를 때 한다. 여기서 던지면 호스트는 "연결 실패"만 보여 주고 이유를 감춘다.
|
|
7
|
+
serveStdio(() => {
|
|
8
|
+
const server = new McpServer({ name: "phalanx", version: "0.1.0" });
|
|
9
|
+
registerTools(server, () => createClient(readEnv(process.env)));
|
|
10
|
+
return server;
|
|
11
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { run } from "./shared.js";
|
|
3
|
+
export function registerGetNamingRules(server, getClient) {
|
|
4
|
+
server.registerTool("get_naming_rules", {
|
|
5
|
+
description: "이 프로젝트의 리소스 이름 규칙과 카테고리별 폴더. 리소스 이름을 짓거나 코드에서 경로를 추측할 때 본다.",
|
|
6
|
+
inputSchema: z.object({ project: z.string().describe("프로젝트 slug") }),
|
|
7
|
+
}, ({ project }) => run(getClient, (client) => client.get(`/api/projects/${encodeURIComponent(project)}/naming-rules`)));
|
|
8
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { registerListProjects } from "./list-projects.js";
|
|
2
|
+
import { registerListRecentFiles } from "./list-recent-files.js";
|
|
3
|
+
import { registerListBundles } from "./list-bundles.js";
|
|
4
|
+
import { registerListBundleFiles } from "./list-bundle-files.js";
|
|
5
|
+
import { registerGetNamingRules } from "./get-naming-rules.js";
|
|
6
|
+
export function registerTools(server, getClient) {
|
|
7
|
+
registerListProjects(server, getClient);
|
|
8
|
+
registerListRecentFiles(server, getClient);
|
|
9
|
+
registerListBundles(server, getClient);
|
|
10
|
+
registerListBundleFiles(server, getClient);
|
|
11
|
+
registerGetNamingRules(server, getClient);
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { run } from "./shared.js";
|
|
3
|
+
export function registerListBundleFiles(server, getClient) {
|
|
4
|
+
server.registerTool("list_bundle_files", {
|
|
5
|
+
description: "묶음 하나의 파일 전체. error 상태 파일의 error 필드가 왜 막혔는지, result.warnings가 확인할 점을 담는다.",
|
|
6
|
+
inputSchema: z.object({ bundle_id: z.string().uuid().describe("list_bundles가 돌려준 id") }),
|
|
7
|
+
}, ({ bundle_id }) => run(getClient, (client) => client.get(`/api/bundles/${bundle_id}/files`)));
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { run } from "./shared.js";
|
|
3
|
+
const STATUSES = [
|
|
4
|
+
"uploaded",
|
|
5
|
+
"analyzing",
|
|
6
|
+
"needs_art_fix",
|
|
7
|
+
"branch_ready",
|
|
8
|
+
"in_review",
|
|
9
|
+
"merged",
|
|
10
|
+
"rejected",
|
|
11
|
+
];
|
|
12
|
+
export function registerListBundles(server, getClient) {
|
|
13
|
+
server.registerTool("list_bundles", {
|
|
14
|
+
description: "리소스 묶음(업로드 단위) 목록. needs_art_fix는 막힌 파일이 있어 사람 처리가 필요한 묶음, in_review는 승인 대기다.",
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
project: z.string().describe("프로젝트 slug"),
|
|
17
|
+
status: z.enum(STATUSES).optional().describe("이 상태만"),
|
|
18
|
+
}),
|
|
19
|
+
}, ({ project, status }) => run(getClient, (client) => client.get(`/api/projects/${encodeURIComponent(project)}/bundles`, { status })));
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { run } from "./shared.js";
|
|
3
|
+
export function registerListProjects(server, getClient) {
|
|
4
|
+
server.registerTool("list_projects", {
|
|
5
|
+
description: "내가 접근할 수 있는 프로젝트 목록. 다른 도구의 project 인자에는 여기 나온 slug를 넣는다.",
|
|
6
|
+
inputSchema: z.object({}),
|
|
7
|
+
}, () => run(getClient, (client) => client.get("/api/projects")));
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { run } from "./shared.js";
|
|
3
|
+
export function registerListRecentFiles(server, getClient) {
|
|
4
|
+
server.registerTool("list_recent_files", {
|
|
5
|
+
description: "이름이 확정된 리소스를 최근 순으로. status가 committed면 레포에 들어가 targetPath를 코드에 바로 쓸 수 있고, proposed면 승인 대기 중이라 경로가 바뀔 수 있다.",
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
project: z.string().describe("프로젝트 slug"),
|
|
8
|
+
limit: z.number().int().min(1).max(200).optional().describe("최대 개수. 기본 60"),
|
|
9
|
+
}),
|
|
10
|
+
}, ({ project, limit }) => run(getClient, (client) => client.get(`/api/projects/${encodeURIComponent(project)}/resources`, {
|
|
11
|
+
limit: limit === undefined ? undefined : String(limit),
|
|
12
|
+
})));
|
|
13
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// 결과는 JSON 문자열 하나로 넘긴다. 호스트 모델이 그대로 읽는다.
|
|
2
|
+
export function textResult(data) {
|
|
3
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
4
|
+
}
|
|
5
|
+
// 오류는 isError로 표시해 모델이 사용자에게 문구를 전달하게 한다. 예외로 던지면 호스트가 삼킨다.
|
|
6
|
+
export function errorResult(error) {
|
|
7
|
+
return {
|
|
8
|
+
isError: true,
|
|
9
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export async function run(getClient, call) {
|
|
13
|
+
try {
|
|
14
|
+
return textResult(await call(getClient()));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return errorResult(error);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "phalanx-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "gamedev-phalanx MCP server. 개인 AI에서 프로젝트·리소스·묶음·네이밍 규칙을 읽는다.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "phalanx-mcp": "./dist/index.js" },
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "node --test \"src/**/*.test.ts\"",
|
|
11
|
+
"prepublishOnly": "pnpm build"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/server": "^2.0.0",
|
|
15
|
+
"zod": "^4.5.4"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^24",
|
|
19
|
+
"typescript": "^5"
|
|
20
|
+
},
|
|
21
|
+
"engines": { "node": ">=20" },
|
|
22
|
+
"license": "UNLICENSED"
|
|
23
|
+
}
|