@rubyjobs-jp/tanebi-mcp-server 1.0.2
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/.github/workflows/publish.yml +26 -0
- package/README.md +82 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +148 -0
- package/package.json +24 -0
- package/src/index.ts +282 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ['v*']
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 20
|
|
18
|
+
registry-url: https://registry.npmjs.org
|
|
19
|
+
|
|
20
|
+
- run: npm ci
|
|
21
|
+
|
|
22
|
+
- run: npm run build
|
|
23
|
+
|
|
24
|
+
- run: npm publish --access public
|
|
25
|
+
env:
|
|
26
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Tanebi MCP Server
|
|
2
|
+
|
|
3
|
+
Tanebi API にアクセスするための [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) サーバー。
|
|
4
|
+
Claude Code などの AI ツールから企画の一覧取得・詳細取得・新規作成が可能。
|
|
5
|
+
|
|
6
|
+
## セットアップ
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
cd mcp-server
|
|
10
|
+
npm install
|
|
11
|
+
npm run build
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 環境変数
|
|
15
|
+
|
|
16
|
+
| 変数名 | 必須 | 説明 |
|
|
17
|
+
|--------|------|------|
|
|
18
|
+
| `TANEBI_API_KEY` | Yes | Tanebi iOS アプリの設定画面から発行した API Key |
|
|
19
|
+
| `TANEBI_API_BASE_URL` | No | API のベース URL(デフォルト: `http://localhost:3000`) |
|
|
20
|
+
|
|
21
|
+
## 提供ツール
|
|
22
|
+
|
|
23
|
+
### `list_ideas` - 企画一覧取得
|
|
24
|
+
|
|
25
|
+
公開されている企画の一覧を取得する。
|
|
26
|
+
|
|
27
|
+
| パラメータ | 型 | 必須 | デフォルト | 説明 |
|
|
28
|
+
|-----------|------|------|-----------|------|
|
|
29
|
+
| `page` | number | No | 1 | ページ番号 |
|
|
30
|
+
| `per_page` | number | No | 20 | 1ページあたりの件数(最大100) |
|
|
31
|
+
|
|
32
|
+
### `get_idea` - 企画詳細取得
|
|
33
|
+
|
|
34
|
+
指定した企画の詳細情報(本文ライン・リアクション含む)を取得する。
|
|
35
|
+
|
|
36
|
+
| パラメータ | 型 | 必須 | 説明 |
|
|
37
|
+
|-----------|------|------|------|
|
|
38
|
+
| `idea_id` | number | Yes | 企画の ID |
|
|
39
|
+
|
|
40
|
+
### `create_idea` - 企画新規作成
|
|
41
|
+
|
|
42
|
+
新しい企画を作成する。`content` は空行区切りで段落に分割され、`#` で始まる段落は見出しになる。
|
|
43
|
+
|
|
44
|
+
| パラメータ | 型 | 必須 | デフォルト | 説明 |
|
|
45
|
+
|-----------|------|------|-----------|------|
|
|
46
|
+
| `title` | string | Yes | - | 企画タイトル |
|
|
47
|
+
| `content` | string | No | - | 企画の本文 |
|
|
48
|
+
| `visibility` | `"public"` \| `"private"` | No | `"public"` | 公開範囲 |
|
|
49
|
+
|
|
50
|
+
## Claude Code での設定
|
|
51
|
+
|
|
52
|
+
`~/.claude.json` または `.claude/settings.json` に追加:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"tanebi": {
|
|
58
|
+
"command": "node",
|
|
59
|
+
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
|
|
60
|
+
"env": {
|
|
61
|
+
"TANEBI_API_KEY": "your-api-key"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
開発時は `tsx` で直接実行も可能:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"tanebi": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["tsx", "/absolute/path/to/mcp-server/src/index.ts"],
|
|
76
|
+
"env": {
|
|
77
|
+
"TANEBI_API_KEY": "your-api-key"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
// --- Configuration ---
|
|
6
|
+
const API_KEY = process.env.TANEBI_API_KEY;
|
|
7
|
+
const API_BASE_URL = process.env.TANEBI_API_BASE_URL?.replace(/\/+$/, "") ||
|
|
8
|
+
"http://localhost:3000";
|
|
9
|
+
if (!API_KEY) {
|
|
10
|
+
console.error("Error: TANEBI_API_KEY environment variable is required.\n" +
|
|
11
|
+
"Generate an API key from the Tanebi iOS app settings.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
async function apiRequest(path, options = {}) {
|
|
15
|
+
const { method = "GET", body } = options;
|
|
16
|
+
const headers = {
|
|
17
|
+
"X-API-Key": API_KEY,
|
|
18
|
+
Accept: "application/json",
|
|
19
|
+
};
|
|
20
|
+
if (body) {
|
|
21
|
+
headers["Content-Type"] = "application/json";
|
|
22
|
+
}
|
|
23
|
+
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const errorBody = await response.text();
|
|
30
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorBody}`);
|
|
31
|
+
}
|
|
32
|
+
return response.json();
|
|
33
|
+
}
|
|
34
|
+
// --- Formatters ---
|
|
35
|
+
function formatIdeaSummary(idea) {
|
|
36
|
+
return [
|
|
37
|
+
`[ID: ${idea.id}] ${idea.title}`,
|
|
38
|
+
` Stage: ${idea.current_stage} | Visibility: ${idea.visibility}`,
|
|
39
|
+
` Author: ${idea.user.display_name}`,
|
|
40
|
+
` Reactions: ${idea.reactions_count} | Comments: ${idea.comments_count}`,
|
|
41
|
+
` Created: ${idea.created_at}`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
function formatIdeaDetail(idea) {
|
|
45
|
+
const header = [
|
|
46
|
+
`# ${idea.title}`,
|
|
47
|
+
"",
|
|
48
|
+
`ID: ${idea.id}`,
|
|
49
|
+
`Stage: ${idea.current_stage} | Visibility: ${idea.visibility}`,
|
|
50
|
+
`Author: ${idea.user.display_name}`,
|
|
51
|
+
`Reactions: ${idea.reactions_count}`,
|
|
52
|
+
`Created: ${idea.created_at} | Updated: ${idea.updated_at}`,
|
|
53
|
+
].join("\n");
|
|
54
|
+
const content = idea.lines.length > 0
|
|
55
|
+
? "\n\n## Content\n\n" +
|
|
56
|
+
idea.lines
|
|
57
|
+
.sort((a, b) => a.position - b.position)
|
|
58
|
+
.map((line) => {
|
|
59
|
+
const prefix = line.line_type === "heading" ? "### " : "";
|
|
60
|
+
const commentTag = line.comments_count > 0 ? ` [${line.comments_count} comments]` : "";
|
|
61
|
+
return `${prefix}${line.content}${commentTag}`;
|
|
62
|
+
})
|
|
63
|
+
.join("\n\n")
|
|
64
|
+
: "\n\n(No content yet)";
|
|
65
|
+
const reactions = idea.reactions.length > 0
|
|
66
|
+
? "\n\n## Reactions\n\n" +
|
|
67
|
+
idea.reactions
|
|
68
|
+
.map((r) => `- ${r.reaction_type} by ${r.user.display_name}`)
|
|
69
|
+
.join("\n")
|
|
70
|
+
: "";
|
|
71
|
+
return header + content + reactions;
|
|
72
|
+
}
|
|
73
|
+
// --- MCP Server ---
|
|
74
|
+
const server = new McpServer({
|
|
75
|
+
name: "tanebi",
|
|
76
|
+
version: "1.0.0",
|
|
77
|
+
});
|
|
78
|
+
// Tool: list_ideas
|
|
79
|
+
server.tool("list_ideas", "List ideas from Tanebi. Returns summaries with title, stage, author, and counts.", {
|
|
80
|
+
page: z
|
|
81
|
+
.number()
|
|
82
|
+
.int()
|
|
83
|
+
.positive()
|
|
84
|
+
.default(1)
|
|
85
|
+
.describe("Page number (default: 1)"),
|
|
86
|
+
per_page: z
|
|
87
|
+
.number()
|
|
88
|
+
.int()
|
|
89
|
+
.positive()
|
|
90
|
+
.max(100)
|
|
91
|
+
.default(20)
|
|
92
|
+
.describe("Items per page (default: 20, max: 100)"),
|
|
93
|
+
}, async ({ page, per_page }) => {
|
|
94
|
+
const data = await apiRequest(`/api/v1/ideas?page=${page}&per_page=${per_page}`);
|
|
95
|
+
const summaries = data.ideas.map(formatIdeaSummary).join("\n\n");
|
|
96
|
+
const pagination = `\nPage ${data.meta.current_page} of ${data.meta.total_pages} (${data.meta.total_count} total ideas)`;
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: summaries + "\n" + pagination }],
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
// Tool: get_idea
|
|
102
|
+
server.tool("get_idea", "Get detailed information about a specific idea, including its content lines and reactions.", {
|
|
103
|
+
idea_id: z.number().int().positive().describe("The ID of the idea to retrieve"),
|
|
104
|
+
}, async ({ idea_id }) => {
|
|
105
|
+
const data = await apiRequest(`/api/v1/ideas/${idea_id}`);
|
|
106
|
+
// The API may return the idea directly or wrapped in { idea: ... }
|
|
107
|
+
const idea = "idea" in data ? data.idea : data;
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: formatIdeaDetail(idea) }],
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
// Tool: create_idea
|
|
113
|
+
server.tool("create_idea", "Create a new idea on Tanebi. Content is split into paragraphs (separated by blank lines) and stored as lines.", {
|
|
114
|
+
title: z.string().min(1).describe("Title of the idea"),
|
|
115
|
+
content: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("Content of the idea. Paragraphs are separated by blank lines. Lines starting with # become headings."),
|
|
119
|
+
visibility: z
|
|
120
|
+
.enum(["public", "private"])
|
|
121
|
+
.default("public")
|
|
122
|
+
.describe('Visibility: "public" or "private" (default: "public")'),
|
|
123
|
+
}, async ({ title, content, visibility }) => {
|
|
124
|
+
const body = { title, visibility };
|
|
125
|
+
if (content) {
|
|
126
|
+
body.content = content;
|
|
127
|
+
}
|
|
128
|
+
const data = await apiRequest("/api/v1/ideas", { method: "POST", body });
|
|
129
|
+
const idea = "idea" in data ? data.idea : data;
|
|
130
|
+
return {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: `Idea created successfully!\n\n${formatIdeaDetail(idea)}`,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
// --- Start Server ---
|
|
140
|
+
async function main() {
|
|
141
|
+
const transport = new StdioServerTransport();
|
|
142
|
+
await server.connect(transport);
|
|
143
|
+
}
|
|
144
|
+
main().catch((error) => {
|
|
145
|
+
console.error("Failed to start MCP server:", error);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
});
|
|
148
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rubyjobs-jp/tanebi-mcp-server",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "MCP server for Tanebi API - list, get, and create ideas",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tanebi-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
17
|
+
"zod": "^3.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"tsx": "^4.0.0",
|
|
22
|
+
"typescript": "^5.7.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
// --- Configuration ---
|
|
8
|
+
|
|
9
|
+
const API_KEY = process.env.TANEBI_API_KEY;
|
|
10
|
+
const API_BASE_URL =
|
|
11
|
+
process.env.TANEBI_API_BASE_URL?.replace(/\/+$/, "") ||
|
|
12
|
+
"http://localhost:3000";
|
|
13
|
+
|
|
14
|
+
if (!API_KEY) {
|
|
15
|
+
console.error(
|
|
16
|
+
"Error: TANEBI_API_KEY environment variable is required.\n" +
|
|
17
|
+
"Generate an API key from the Tanebi iOS app settings."
|
|
18
|
+
);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- API Client ---
|
|
23
|
+
|
|
24
|
+
interface ApiRequestOptions {
|
|
25
|
+
method?: string;
|
|
26
|
+
body?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function apiRequest<T>(
|
|
30
|
+
path: string,
|
|
31
|
+
options: ApiRequestOptions = {}
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
const { method = "GET", body } = options;
|
|
34
|
+
|
|
35
|
+
const headers: Record<string, string> = {
|
|
36
|
+
"X-API-Key": API_KEY!,
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (body) {
|
|
41
|
+
headers["Content-Type"] = "application/json";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const response = await fetch(`${API_BASE_URL}${path}`, {
|
|
45
|
+
method,
|
|
46
|
+
headers,
|
|
47
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const errorBody = await response.text();
|
|
52
|
+
throw new Error(
|
|
53
|
+
`API request failed: ${response.status} ${response.statusText} - ${errorBody}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return response.json() as Promise<T>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- Response Types ---
|
|
61
|
+
|
|
62
|
+
interface SimplifiedUser {
|
|
63
|
+
id: number;
|
|
64
|
+
display_name: string;
|
|
65
|
+
avatar_path: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface IdeaSummary {
|
|
69
|
+
id: number;
|
|
70
|
+
title: string;
|
|
71
|
+
visibility: string;
|
|
72
|
+
current_stage: string;
|
|
73
|
+
reactions_count: number;
|
|
74
|
+
comments_count: number;
|
|
75
|
+
is_bookmarked: boolean;
|
|
76
|
+
created_at: string;
|
|
77
|
+
updated_at: string;
|
|
78
|
+
user: SimplifiedUser;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface IdeaLine {
|
|
82
|
+
id: number;
|
|
83
|
+
line_type: string;
|
|
84
|
+
content: string;
|
|
85
|
+
position: number;
|
|
86
|
+
comments_count: number;
|
|
87
|
+
created_at: string;
|
|
88
|
+
updated_at: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface Reaction {
|
|
92
|
+
id: number;
|
|
93
|
+
reaction_type: string;
|
|
94
|
+
created_at: string;
|
|
95
|
+
user: SimplifiedUser;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface IdeaDetail {
|
|
99
|
+
id: number;
|
|
100
|
+
title: string;
|
|
101
|
+
visibility: string;
|
|
102
|
+
current_stage: string;
|
|
103
|
+
reactions_count: number;
|
|
104
|
+
is_bookmarked: boolean;
|
|
105
|
+
created_at: string;
|
|
106
|
+
updated_at: string;
|
|
107
|
+
user: SimplifiedUser;
|
|
108
|
+
lines: IdeaLine[];
|
|
109
|
+
reactions: Reaction[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface PaginationMeta {
|
|
113
|
+
current_page: number;
|
|
114
|
+
total_pages: number;
|
|
115
|
+
total_count: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface IdeasListResponse {
|
|
119
|
+
ideas: IdeaSummary[];
|
|
120
|
+
meta: PaginationMeta;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Formatters ---
|
|
124
|
+
|
|
125
|
+
function formatIdeaSummary(idea: IdeaSummary): string {
|
|
126
|
+
return [
|
|
127
|
+
`[ID: ${idea.id}] ${idea.title}`,
|
|
128
|
+
` Stage: ${idea.current_stage} | Visibility: ${idea.visibility}`,
|
|
129
|
+
` Author: ${idea.user.display_name}`,
|
|
130
|
+
` Reactions: ${idea.reactions_count} | Comments: ${idea.comments_count}`,
|
|
131
|
+
` Created: ${idea.created_at}`,
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatIdeaDetail(idea: IdeaDetail): string {
|
|
136
|
+
const header = [
|
|
137
|
+
`# ${idea.title}`,
|
|
138
|
+
"",
|
|
139
|
+
`ID: ${idea.id}`,
|
|
140
|
+
`Stage: ${idea.current_stage} | Visibility: ${idea.visibility}`,
|
|
141
|
+
`Author: ${idea.user.display_name}`,
|
|
142
|
+
`Reactions: ${idea.reactions_count}`,
|
|
143
|
+
`Created: ${idea.created_at} | Updated: ${idea.updated_at}`,
|
|
144
|
+
].join("\n");
|
|
145
|
+
|
|
146
|
+
const content =
|
|
147
|
+
idea.lines.length > 0
|
|
148
|
+
? "\n\n## Content\n\n" +
|
|
149
|
+
idea.lines
|
|
150
|
+
.sort((a, b) => a.position - b.position)
|
|
151
|
+
.map((line) => {
|
|
152
|
+
const prefix = line.line_type === "heading" ? "### " : "";
|
|
153
|
+
const commentTag = line.comments_count > 0 ? ` [${line.comments_count} comments]` : "";
|
|
154
|
+
return `${prefix}${line.content}${commentTag}`;
|
|
155
|
+
})
|
|
156
|
+
.join("\n\n")
|
|
157
|
+
: "\n\n(No content yet)";
|
|
158
|
+
|
|
159
|
+
const reactions =
|
|
160
|
+
idea.reactions.length > 0
|
|
161
|
+
? "\n\n## Reactions\n\n" +
|
|
162
|
+
idea.reactions
|
|
163
|
+
.map((r) => `- ${r.reaction_type} by ${r.user.display_name}`)
|
|
164
|
+
.join("\n")
|
|
165
|
+
: "";
|
|
166
|
+
|
|
167
|
+
return header + content + reactions;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- MCP Server ---
|
|
171
|
+
|
|
172
|
+
const server = new McpServer({
|
|
173
|
+
name: "tanebi",
|
|
174
|
+
version: "1.0.0",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Tool: list_ideas
|
|
178
|
+
server.tool(
|
|
179
|
+
"list_ideas",
|
|
180
|
+
"List ideas from Tanebi. Returns summaries with title, stage, author, and counts.",
|
|
181
|
+
{
|
|
182
|
+
page: z
|
|
183
|
+
.number()
|
|
184
|
+
.int()
|
|
185
|
+
.positive()
|
|
186
|
+
.default(1)
|
|
187
|
+
.describe("Page number (default: 1)"),
|
|
188
|
+
per_page: z
|
|
189
|
+
.number()
|
|
190
|
+
.int()
|
|
191
|
+
.positive()
|
|
192
|
+
.max(100)
|
|
193
|
+
.default(20)
|
|
194
|
+
.describe("Items per page (default: 20, max: 100)"),
|
|
195
|
+
},
|
|
196
|
+
async ({ page, per_page }) => {
|
|
197
|
+
const data = await apiRequest<IdeasListResponse>(
|
|
198
|
+
`/api/v1/ideas?page=${page}&per_page=${per_page}`
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const summaries = data.ideas.map(formatIdeaSummary).join("\n\n");
|
|
202
|
+
const pagination = `\nPage ${data.meta.current_page} of ${data.meta.total_pages} (${data.meta.total_count} total ideas)`;
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: "text" as const, text: summaries + "\n" + pagination }],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Tool: get_idea
|
|
211
|
+
server.tool(
|
|
212
|
+
"get_idea",
|
|
213
|
+
"Get detailed information about a specific idea, including its content lines and reactions.",
|
|
214
|
+
{
|
|
215
|
+
idea_id: z.number().int().positive().describe("The ID of the idea to retrieve"),
|
|
216
|
+
},
|
|
217
|
+
async ({ idea_id }) => {
|
|
218
|
+
const data = await apiRequest<{ idea: IdeaDetail }>(
|
|
219
|
+
`/api/v1/ideas/${idea_id}`
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// The API may return the idea directly or wrapped in { idea: ... }
|
|
223
|
+
const idea = "idea" in data ? data.idea : (data as unknown as IdeaDetail);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: "text" as const, text: formatIdeaDetail(idea) }],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Tool: create_idea
|
|
232
|
+
server.tool(
|
|
233
|
+
"create_idea",
|
|
234
|
+
"Create a new idea on Tanebi. Content is split into paragraphs (separated by blank lines) and stored as lines.",
|
|
235
|
+
{
|
|
236
|
+
title: z.string().min(1).describe("Title of the idea"),
|
|
237
|
+
content: z
|
|
238
|
+
.string()
|
|
239
|
+
.optional()
|
|
240
|
+
.describe(
|
|
241
|
+
"Content of the idea. Paragraphs are separated by blank lines. Lines starting with # become headings."
|
|
242
|
+
),
|
|
243
|
+
visibility: z
|
|
244
|
+
.enum(["public", "private"])
|
|
245
|
+
.default("public")
|
|
246
|
+
.describe('Visibility: "public" or "private" (default: "public")'),
|
|
247
|
+
},
|
|
248
|
+
async ({ title, content, visibility }) => {
|
|
249
|
+
const body: Record<string, unknown> = { title, visibility };
|
|
250
|
+
if (content) {
|
|
251
|
+
body.content = content;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const data = await apiRequest<{ idea: IdeaDetail } | IdeaDetail>(
|
|
255
|
+
"/api/v1/ideas",
|
|
256
|
+
{ method: "POST", body }
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const idea = "idea" in data ? data.idea : data;
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: "text" as const,
|
|
265
|
+
text: `Idea created successfully!\n\n${formatIdeaDetail(idea)}`,
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// --- Start Server ---
|
|
273
|
+
|
|
274
|
+
async function main() {
|
|
275
|
+
const transport = new StdioServerTransport();
|
|
276
|
+
await server.connect(transport);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
main().catch((error) => {
|
|
280
|
+
console.error("Failed to start MCP server:", error);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|