craft-stadium-mcp 1.0.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/README.md +48 -0
- package/dist/client.js +75 -0
- package/dist/index.js +63 -0
- package/dist/tool-helpers.js +51 -0
- package/dist/types.js +2 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# craft-stadium-mcp
|
|
2
|
+
|
|
3
|
+
CraftStadiumをClaude(Desktop/Code)から直接操作するためのMCPサーバー。組織のPersonal Access Token(PAT)で認証する。
|
|
4
|
+
|
|
5
|
+
## セットアップ
|
|
6
|
+
|
|
7
|
+
1. Organizeアプリの「設定 > アクセストークン」からPATを発行する。
|
|
8
|
+
2. Claude Desktop/Codeの設定(`claude_desktop_config.json` または `.mcp.json`)に追加:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"craft-stadium": {
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["craft-stadium-mcp"],
|
|
16
|
+
"env": {
|
|
17
|
+
"CRAFTSTADIUM_TOKEN": "cs_pat_..."
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`npx`が都度パッケージを取得して実行するため、事前インストールは不要。デフォルトで本番API(`https://api.craftstadium.com`)に接続する。社内の開発環境(docker compose)に向けたい場合は `CRAFTSTADIUM_API_URL` に `http://localhost:8080` を追加で設定する。
|
|
25
|
+
|
|
26
|
+
## 提供ツール
|
|
27
|
+
|
|
28
|
+
| ツール | 内容 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `whoami` | 組織名・アカウント名・ロールを取得 |
|
|
31
|
+
| `list_organizers` | 主催者一覧 |
|
|
32
|
+
| `create_plan` | 企画作成 |
|
|
33
|
+
| `list_plans` | 企画一覧 |
|
|
34
|
+
| `create_hackathon` | 下書きハッカソン作成(公開設定・日程はWeb UIから編集) |
|
|
35
|
+
| `list_hackathons` | 企画配下のハッカソン一覧 |
|
|
36
|
+
|
|
37
|
+
## 開発
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm run dev # tsx で直接起動
|
|
41
|
+
npx tsc --noEmit # 型チェック
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
MCP Inspectorで動作確認する場合:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @modelcontextprotocol/inspector npx tsx src/index.ts
|
|
48
|
+
```
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// cli/internal/client/client.go の TypeScript 版。
|
|
2
|
+
export class APIError extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
constructor(statusCode, message) {
|
|
5
|
+
super(`${message} (status ${statusCode})`);
|
|
6
|
+
this.statusCode = statusCode;
|
|
7
|
+
this.name = "APIError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class Client {
|
|
11
|
+
baseURL;
|
|
12
|
+
token;
|
|
13
|
+
constructor(baseURL, token) {
|
|
14
|
+
this.baseURL = baseURL.replace(/\/+$/, "");
|
|
15
|
+
this.token = token;
|
|
16
|
+
}
|
|
17
|
+
async do(method, path, body) {
|
|
18
|
+
const headers = { "Content-Type": "application/json" };
|
|
19
|
+
if (this.token !== "") {
|
|
20
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
21
|
+
}
|
|
22
|
+
const res = await fetch(this.baseURL + path, {
|
|
23
|
+
method,
|
|
24
|
+
headers,
|
|
25
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
26
|
+
// cli/internal/client/client.go の http.Client{Timeout: 30 * time.Second} に合わせる。
|
|
27
|
+
signal: AbortSignal.timeout(30_000),
|
|
28
|
+
});
|
|
29
|
+
const text = await res.text();
|
|
30
|
+
if (res.status >= 300) {
|
|
31
|
+
let message = text;
|
|
32
|
+
try {
|
|
33
|
+
const eb = JSON.parse(text);
|
|
34
|
+
if (eb.message)
|
|
35
|
+
message = eb.message;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// レスポンスがJSONでない場合は生テキストをそのまま使う
|
|
39
|
+
}
|
|
40
|
+
throw new APIError(res.status, message);
|
|
41
|
+
}
|
|
42
|
+
if (text.length === 0) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
}
|
|
47
|
+
get(path) {
|
|
48
|
+
return this.do("GET", path);
|
|
49
|
+
}
|
|
50
|
+
post(path, body) {
|
|
51
|
+
return this.do("POST", path, body);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// 外部主催者向けの既定値。社内の開発環境(docker compose)に向けたい場合は
|
|
55
|
+
// CRAFTSTADIUM_API_URL=http://localhost:8080 を明示的に設定する。
|
|
56
|
+
const DEFAULT_API_URL = "https://api.craftstadium.com";
|
|
57
|
+
function newClient() {
|
|
58
|
+
const envURL = process.env.CRAFTSTADIUM_API_URL;
|
|
59
|
+
const baseURL = envURL && envURL !== "" ? envURL : DEFAULT_API_URL;
|
|
60
|
+
const token = process.env.CRAFTSTADIUM_TOKEN;
|
|
61
|
+
if (!token) {
|
|
62
|
+
throw new Error("CRAFTSTADIUM_TOKEN が設定されていません。MCPサーバー設定のenvにPersonal Access Tokenを指定してください。");
|
|
63
|
+
}
|
|
64
|
+
return new Client(baseURL, token);
|
|
65
|
+
}
|
|
66
|
+
let cached;
|
|
67
|
+
// トークン未設定時のエラーをツール呼び出し時まで遅延させる。モジュール読み込み時に
|
|
68
|
+
// newClient()を直接呼ぶと、MCPトランスポート接続前にプロセスがクラッシュし、
|
|
69
|
+
// MCPホスト側にエラーメッセージが伝わらない。
|
|
70
|
+
export function getClient() {
|
|
71
|
+
if (!cached) {
|
|
72
|
+
cached = newClient();
|
|
73
|
+
}
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getClient } from "./client.js";
|
|
6
|
+
import { buildCreateHackathonRequest, buildCreatePlanRequest, withTool } from "./tool-helpers.js";
|
|
7
|
+
const server = new McpServer({ name: "craft-stadium", version: "1.0.0" });
|
|
8
|
+
const planIdSchema = z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1, "planIdを指定してください(list_plansで確認できる)")
|
|
11
|
+
.describe("企画ID(list_plansで確認できる)");
|
|
12
|
+
server.tool("whoami", "現在のPATに紐づく組織名・アカウント名・ロールを取得する", {}, withTool("whoamiの取得に失敗しました", async () => {
|
|
13
|
+
const client = getClient();
|
|
14
|
+
const [org, account] = await Promise.all([
|
|
15
|
+
client.get("/organizations/my-organization"),
|
|
16
|
+
client.get("/organizations/my-organization/accounts/me"),
|
|
17
|
+
]);
|
|
18
|
+
return {
|
|
19
|
+
organizationId: org.id,
|
|
20
|
+
organizationName: org.info.name,
|
|
21
|
+
accountId: account.id,
|
|
22
|
+
accountName: account.name,
|
|
23
|
+
role: account.role,
|
|
24
|
+
};
|
|
25
|
+
}));
|
|
26
|
+
server.tool("list_organizers", "組織配下の主催者一覧を取得する", {}, withTool("主催者一覧の取得に失敗しました", async () => {
|
|
27
|
+
return getClient().get("/organizations/my-organization/organizers");
|
|
28
|
+
}));
|
|
29
|
+
server.tool("create_plan", "新しい企画(Plan)を作成する", {
|
|
30
|
+
title: z.string().min(1, "titleを指定してください").describe("企画タイトル"),
|
|
31
|
+
organizerId: z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.positive("organizerIdを指定してください(list_organizersで確認できる)")
|
|
35
|
+
.describe("主催者ID(list_organizersで確認できる)"),
|
|
36
|
+
sponsorRecruitment: z.boolean().default(false).describe("スポンサー募集を有効にするか"),
|
|
37
|
+
operatingSupport: z.boolean().default(false).describe("運営サポートを有効にするか"),
|
|
38
|
+
}, withTool("企画の作成に失敗しました", async ({ title, organizerId, sponsorRecruitment, operatingSupport }) => {
|
|
39
|
+
const req = buildCreatePlanRequest({ title, organizerId, sponsorRecruitment, operatingSupport });
|
|
40
|
+
return getClient().post("/plans", req);
|
|
41
|
+
}));
|
|
42
|
+
server.tool("list_plans", "企画(Plan)の一覧を取得する", {}, withTool("企画一覧の取得に失敗しました", async () => {
|
|
43
|
+
return getClient().get("/plans");
|
|
44
|
+
}));
|
|
45
|
+
server.tool("create_hackathon", "指定した企画の下に下書きハッカソンを作成する。公開設定・日程等はWeb UIから後で編集する前提。", {
|
|
46
|
+
planId: planIdSchema,
|
|
47
|
+
title: z.string().min(1, "titleを指定してください").describe("ハッカソンタイトル"),
|
|
48
|
+
displayId: z
|
|
49
|
+
.string()
|
|
50
|
+
.min(1)
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("ハッカソンの表示ID(省略時はタイトルから自動生成)"),
|
|
53
|
+
}, withTool("ハッカソンの作成に失敗しました", async ({ planId, title, displayId }) => {
|
|
54
|
+
const req = buildCreateHackathonRequest({ title, displayId });
|
|
55
|
+
return getClient().post(`/plans/${encodeURIComponent(planId)}/hackathons`, req);
|
|
56
|
+
}));
|
|
57
|
+
server.tool("list_hackathons", "指定した企画に紐づくハッカソンの一覧を取得する", {
|
|
58
|
+
planId: planIdSchema,
|
|
59
|
+
}, withTool("ハッカソン一覧の取得に失敗しました", async ({ planId }) => {
|
|
60
|
+
return getClient().get(`/plans/${encodeURIComponent(planId)}/hackathons`);
|
|
61
|
+
}));
|
|
62
|
+
const transport = new StdioServerTransport();
|
|
63
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { APIError } from "./client.js";
|
|
3
|
+
// cli/cmd/hackathon.go の generateDisplayId と同じロジック(スラッグ化+ランダムサフィックス)。
|
|
4
|
+
export function generateDisplayId(title) {
|
|
5
|
+
let slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
6
|
+
slug = slug.replace(/^-+|-+$/g, "");
|
|
7
|
+
if (slug === "")
|
|
8
|
+
slug = "hackathon";
|
|
9
|
+
const suffix = randomBytes(4).toString("hex");
|
|
10
|
+
return `${slug}-${suffix}`;
|
|
11
|
+
}
|
|
12
|
+
export function buildCreatePlanRequest(args) {
|
|
13
|
+
const hackathonOptions = {
|
|
14
|
+
sponsorRecruitment: args.sponsorRecruitment,
|
|
15
|
+
operatingSupport: args.operatingSupport,
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
title: args.title,
|
|
19
|
+
organizerId: args.organizerId,
|
|
20
|
+
hackathonOptions,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
// displayId省略時はgenerateDisplayIdで自動生成する。日程・selectMethodは下書き作成時は
|
|
24
|
+
// 省略可 — バックエンドのapplyDraftDefaults(backend/app/internal/usecase/raid.go)が
|
|
25
|
+
// 未入力のままサーバー側でデフォルト値を適用する(Issue #3339対応、PR #3347)。
|
|
26
|
+
export function buildCreateHackathonRequest(args) {
|
|
27
|
+
return {
|
|
28
|
+
isPublished: false,
|
|
29
|
+
info: {
|
|
30
|
+
displayId: args.displayId ?? generateDisplayId(args.title),
|
|
31
|
+
title: args.title,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function textResult(value) {
|
|
36
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
37
|
+
}
|
|
38
|
+
// 各ツールハンドラの成功時整形とエラーへのラベル付与を共通化する。
|
|
39
|
+
// MCP SDK自身がツールコールバックのthrowを{content,isError:true}へ変換するため、
|
|
40
|
+
// ここではエラーメッセージにどのツールで失敗したかのラベルを添えて再throwするだけでよい。
|
|
41
|
+
export function withTool(label, handler) {
|
|
42
|
+
return async (args) => {
|
|
43
|
+
try {
|
|
44
|
+
return textResult(await handler(args));
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
const message = err instanceof APIError ? err.message : err instanceof Error ? err.message : String(err);
|
|
48
|
+
throw new Error(`${label}: ${message}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "craft-stadium-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for operating CraftStadium via organization Personal Access Tokens",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"craft-stadium-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "tsx src/index.ts",
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"test": "node --import tsx --test src/*.test.ts"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
21
|
+
"zod": "^3.24.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.10.0",
|
|
25
|
+
"tsx": "^4.22.4",
|
|
26
|
+
"typescript": "^5.5.0"
|
|
27
|
+
}
|
|
28
|
+
}
|