# Directory Structure
```
├── .clinerules
├── .gitignore
├── assets
│ ├── header.svg
│ └── release-v0.2.0.svg
├── docs
│ └── v0.2.0
│ └── RELEASE.md
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── handlers
│ │ ├── comment-handlers.ts
│ │ ├── index.ts
│ │ ├── issue-handlers.ts
│ │ ├── label-handlers.ts
│ │ └── tool-handlers.ts
│ ├── index.ts
│ ├── schemas
│ │ ├── comment-schemas.ts
│ │ ├── index.ts
│ │ └── issue-schemas.ts
│ ├── server.ts
│ ├── types.ts
│ └── utils
│ ├── error-handler.ts
│ ├── exec.ts
│ └── repo-info.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | build/
3 | .env
4 | .env.local
5 | .DS_Store
6 | coverage/
7 | *.log
8 | .vscode/
9 | tmp
10 | tasks/
11 |
12 |
13 | .codegpt
```
--------------------------------------------------------------------------------
/src/schemas/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export * from './issue-schemas.js';
2 | export * from './comment-schemas.js';
3 |
```
--------------------------------------------------------------------------------
/src/handlers/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export * from './issue-handlers.js';
2 | export * from './label-handlers.js';
3 | export * from './comment-handlers.js';
4 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 | import { KanbanServer } from './server.js';
3 |
4 | const server = new KanbanServer();
5 | server.run().catch(console.error);
6 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@sunwood-ai-labs/github-kanban-mcp-server",
3 | "version": "0.2.0",
4 | "description": "A Model Context Protocol server for managing GitHub issues as Kanban using gh CLI",
5 | "main": "build/index.js",
6 | "type": "module",
7 | "bin": {
8 | "github-kanban-mcp-server": "build/index.js"
9 | },
10 | "scripts": {
11 | "build": "tsc",
12 | "start": "node build/index.js",
13 | "dev": "node --loader ts-node/esm src/index.ts",
14 | "test": "jest",
15 | "prepare": "npm run build"
16 | },
17 | "keywords": [
18 | "mcp",
19 | "github",
20 | "kanban",
21 | "issues",
22 | "llm",
23 | "gh-cli"
24 | ],
25 | "author": "Sunwood AI Labs",
26 | "license": "MIT",
27 | "dependencies": {
28 | "@modelcontextprotocol/sdk": "^1.0.4",
29 | "dotenv": "^16.3.1"
30 | },
31 | "devDependencies": {
32 | "@types/jest": "^29.5.5",
33 | "@types/node": "^20.8.2",
34 | "jest": "^29.7.0",
35 | "ts-jest": "^29.1.1",
36 | "ts-node": "^10.9.1",
37 | "typescript": "^5.2.2"
38 | },
39 | "files": [
40 | "build",
41 | "README.md",
42 | "LICENSE"
43 | ],
44 | "publishConfig": {
45 | "access": "public"
46 | },
47 | "repository": {
48 | "type": "git",
49 | "url": "git+https://github.com/sunwood-ai-labs/github-kanban-mcp-server.git"
50 | },
51 | "bugs": {
52 | "url": "https://github.com/sunwood-ai-labs/github-kanban-mcp-server/issues"
53 | },
54 | "homepage": "https://github.com/sunwood-ai-labs/github-kanban-mcp-server#readme"
55 | }
56 |
```
--------------------------------------------------------------------------------
/src/handlers/label-handlers.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2 | import { execAsync } from '../utils/exec.js';
3 | import { getRepoInfoFromGitConfig } from '../utils/repo-info.js';
4 |
5 | /**
6 | * ランダムな16進数カラーコードを生成する
7 | */
8 | export function generateRandomColor(): string {
9 | const letters = '0123456789ABCDEF';
10 | let color = '#';
11 | for (let i = 0; i < 6; i++) {
12 | color += letters[Math.floor(Math.random() * 16)];
13 | }
14 | return color;
15 | }
16 |
17 | /**
18 | * リポジトリ内の既存のラベルを取得する
19 | */
20 | export async function getExistingLabels(path: string): Promise<string[]> {
21 | try {
22 | const { owner, repo } = await getRepoInfoFromGitConfig(path);
23 | const { stdout } = await execAsync(
24 | `gh label list --repo ${owner}/${repo} --json name --jq '.[].name'`
25 | );
26 | return stdout.trim().split('\n').filter(Boolean);
27 | } catch (error) {
28 | console.error('Failed to get labels:', error);
29 | return [];
30 | }
31 | }
32 |
33 | /**
34 | * 新しいラベルを作成する
35 | */
36 | export async function createLabel(path: string, name: string): Promise<void> {
37 | const color = generateRandomColor().substring(1); // '#'を除去
38 | const { owner, repo } = await getRepoInfoFromGitConfig(path);
39 | try {
40 | await execAsync(
41 | `gh label create "${name}" --repo ${owner}/${repo} --color "${color}" --force`
42 | );
43 | } catch (error) {
44 | console.error(`Failed to create label ${name}:`, error);
45 | throw new McpError(
46 | ErrorCode.InternalError,
47 | `Failed to create label ${name}: ${(error as Error).message}`
48 | );
49 | }
50 | }
51 |
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3 | import {
4 | CallToolRequestSchema,
5 | ListToolsRequestSchema,
6 | } from '@modelcontextprotocol/sdk/types.js';
7 | import {
8 | listIssuesSchema,
9 | createIssueSchema,
10 | updateIssueSchema,
11 | addCommentSchema,
12 | } from './schemas/index.js';
13 | import { handleToolRequest } from './handlers/tool-handlers.js';
14 | import { handleServerError, handleProcessTermination } from './utils/error-handler.js';
15 |
16 | export class KanbanServer {
17 | private server: Server;
18 |
19 | constructor() {
20 | this.server = new Server(
21 | {
22 | name: 'github-kanban-mcp-server',
23 | version: '0.2.0',
24 | },
25 | {
26 | capabilities: {
27 | tools: {},
28 | },
29 | }
30 | );
31 |
32 | this.setupToolHandlers();
33 |
34 | this.server.onerror = handleServerError;
35 | handleProcessTermination(this.server);
36 | }
37 |
38 | private setupToolHandlers(): void {
39 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
40 | tools: [
41 | {
42 | name: 'list_issues',
43 | description: 'カンバンボードのissue一覧を取得します',
44 | inputSchema: listIssuesSchema,
45 | },
46 | {
47 | name: 'create_issue',
48 | description: '新しいissueを作成します',
49 | inputSchema: createIssueSchema,
50 | },
51 | {
52 | name: 'update_issue',
53 | description: '既存のissueを更新します',
54 | inputSchema: updateIssueSchema,
55 | },
56 | {
57 | name: 'add_comment',
58 | description: 'タスクにコメントを追加',
59 | inputSchema: addCommentSchema,
60 | },
61 | ],
62 | }));
63 |
64 | this.server.setRequestHandler(CallToolRequestSchema, handleToolRequest);
65 | }
66 |
67 | public async run(): Promise<void> {
68 | const transport = new StdioServerTransport();
69 | await this.server.connect(transport);
70 | console.error('GitHub Kanban MCP server running on stdio');
71 | }
72 | }
73 |
```
--------------------------------------------------------------------------------
/src/handlers/comment-handlers.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
2 | import { execAsync, writeToTempFile, removeTempFile } from '../utils/exec.js';
3 | import { ToolResponse } from '../types.js';
4 |
5 | /**
6 | * Issueにコメントを追加する
7 | */
8 | export async function handleAddComment(args: {
9 | repo: string;
10 | issue_number: string;
11 | body: string;
12 | state?: 'open' | 'closed';
13 | }): Promise<ToolResponse> {
14 | const tempFile = 'comment_body.md';
15 |
16 | try {
17 | // ステータスの変更が指定されている場合は先に処理
18 | if (args.state) {
19 | try {
20 | const command = args.state === 'closed' ? 'close' : 'reopen';
21 | await execAsync(
22 | `gh issue ${command} ${args.issue_number} --repo ${args.repo}`
23 | );
24 | console.log(`Issue status changed to ${args.state}`);
25 | } catch (error) {
26 | console.error('Failed to change issue status:', error);
27 | throw new McpError(
28 | ErrorCode.InternalError,
29 | `Failed to change issue status: ${(error as Error).message}`
30 | );
31 | }
32 | }
33 |
34 | // コメントを追加
35 | const fullPath = await writeToTempFile(args.body, tempFile);
36 | try {
37 | await execAsync(
38 | `gh issue comment ${args.issue_number} --repo ${args.repo} --body-file "${fullPath}"`
39 | );
40 | } catch (error) {
41 | console.error('Failed to add comment:', error);
42 | throw new McpError(
43 | ErrorCode.InternalError,
44 | `Failed to add comment: ${(error as Error).message}`
45 | );
46 | }
47 |
48 | // 更新後のissue情報を取得して返却
49 | try {
50 | const { stdout: issueData } = await execAsync(
51 | `gh issue view ${args.issue_number} --repo ${args.repo} --json number,title,state,url`
52 | );
53 | return {
54 | content: [
55 | {
56 | type: 'text',
57 | text: issueData,
58 | },
59 | ],
60 | };
61 | } catch (error) {
62 | console.error('Failed to get issue data:', error);
63 | throw new McpError(
64 | ErrorCode.InternalError,
65 | `Failed to get issue data: ${(error as Error).message}`
66 | );
67 | }
68 | } finally {
69 | await removeTempFile(tempFile);
70 | }
71 | }
72 |
```
--------------------------------------------------------------------------------
/src/handlers/tool-handlers.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { CallToolRequest, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2 | import { ToolResponse, RepoArgs } from '../types.js';
3 | import {
4 | handleListIssues,
5 | handleCreateIssue,
6 | handleUpdateIssue,
7 | handleAddComment,
8 | } from './index.js';
9 | import { getRepoInfoFromGitConfig, validateRepoInfo } from '../utils/repo-info.js';
10 |
11 | export async function handleToolRequest(request: CallToolRequest): Promise<ToolResponse> {
12 | try {
13 | const args = request.params.arguments as Record<string, unknown> & RepoArgs;
14 |
15 | // pathパラメータの確認
16 | if (!args.path) {
17 | throw new McpError(
18 | ErrorCode.InvalidParams,
19 | 'リポジトリのパスを指定してください。引数で "path": "/path/to/repo" の形式で指定してください。'
20 | );
21 | }
22 |
23 | // リポジトリ情報の取得
24 | let repoInfo: { owner: string; repo: string };
25 | try {
26 | repoInfo = await getRepoInfoFromGitConfig(args.path as string);
27 | } catch (error) {
28 | // Gitリポジトリ関連のエラーの場合は、より具体的なエラーメッセージを表示
29 | if (error instanceof McpError) {
30 | throw error;
31 | }
32 | throw new McpError(
33 | ErrorCode.InvalidParams,
34 | `指定されたパスのリポジトリ情報の取得に失敗しました: ${(error as Error).message}`
35 | );
36 | }
37 |
38 | const fullRepo = `${repoInfo.owner}/${repoInfo.repo}`;
39 |
40 | switch (request.params.name) {
41 | case 'list_issues':
42 | return await handleListIssues({
43 | path: args.path as string,
44 | state: args?.state as 'open' | 'closed' | 'all',
45 | labels: args?.labels as string[],
46 | });
47 | case 'create_issue': {
48 | if (!args?.title) {
49 | throw new McpError(ErrorCode.InvalidParams, 'Title is required');
50 | }
51 | return await handleCreateIssue({
52 | path: args.path as string,
53 | title: args.title as string,
54 | emoji: args?.emoji as string | undefined,
55 | body: args?.body as string | undefined,
56 | labels: args?.labels as string[] | undefined,
57 | assignees: args?.assignees as string[] | undefined,
58 | });
59 | }
60 | case 'update_issue': {
61 | if (!args?.issue_number) {
62 | throw new McpError(ErrorCode.InvalidParams, 'Issue number is required');
63 | }
64 | return await handleUpdateIssue({
65 | path: args.path as string,
66 | issue_number: Number(args.issue_number),
67 | title: args?.title as string | undefined,
68 | emoji: args?.emoji as string | undefined,
69 | body: args?.body as string | undefined,
70 | state: args?.state as 'open' | 'closed' | undefined,
71 | labels: args?.labels as string[] | undefined,
72 | assignees: args?.assignees as string[] | undefined,
73 | });
74 | }
75 | case 'add_comment': {
76 | if (!args?.issue_number || !args?.body) {
77 | throw new McpError(ErrorCode.InvalidParams, 'Issue number and body are required');
78 | }
79 | return await handleAddComment({
80 | repo: fullRepo,
81 | issue_number: args.issue_number as string,
82 | body: args.body as string,
83 | });
84 | }
85 | default:
86 | throw new McpError(
87 | ErrorCode.MethodNotFound,
88 | `Unknown tool: ${request.params.name}`
89 | );
90 | }
91 | } catch (error) {
92 | if (error instanceof McpError) throw error;
93 | throw new McpError(
94 | ErrorCode.InternalError,
95 | `GitHub API error: ${(error as Error).message}`
96 | );
97 | }
98 | }
99 |
```
--------------------------------------------------------------------------------
/assets/release-v0.2.0.svg:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <svg width="800" height="200" viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg">
3 | <defs>
4 | <!-- メインの背景グラデーション -->
5 | <linearGradient id="headerGradient" x1="0%" y1="0%" x2="100%" y2="100%">
6 | <stop offset="0%" style="stop-color:#3b82f6;stop-opacity:1">
7 | <animate attributeName="stop-color"
8 | values="#3b82f6; #6366f1; #8b5cf6; #3b82f6"
9 | dur="10s"
10 | repeatCount="indefinite" />
11 | </stop>
12 | <stop offset="50%" style="stop-color:#6366f1;stop-opacity:1">
13 | <animate attributeName="stop-color"
14 | values="#6366f1; #8b5cf6; #3b82f6; #6366f1"
15 | dur="10s"
16 | repeatCount="indefinite" />
17 | </stop>
18 | <stop offset="100%" style="stop-color:#8b5cf6;stop-opacity:1">
19 | <animate attributeName="stop-color"
20 | values="#8b5cf6; #3b82f6; #6366f1; #8b5cf6"
21 | dur="10s"
22 | repeatCount="indefinite" />
23 | </stop>
24 | </linearGradient>
25 |
26 | <!-- テキストグラデーション -->
27 | <linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
28 | <stop offset="0%" style="stop-color:#ffffff;stop-opacity:1">
29 | <animate attributeName="stop-color"
30 | values="#ffffff; #ffd700; #ffffff"
31 | dur="3s"
32 | repeatCount="indefinite" />
33 | </stop>
34 | <stop offset="100%" style="stop-color:#ffd700;stop-opacity:1">
35 | <animate attributeName="stop-color"
36 | values="#ffd700; #ffffff; #ffd700"
37 | dur="3s"
38 | repeatCount="indefinite" />
39 | </stop>
40 | </linearGradient>
41 |
42 | <!-- 装飾用のパターン -->
43 | <pattern id="dots" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
44 | <circle cx="2" cy="2" r="1" fill="#fff" opacity="0.2">
45 | <animate attributeName="opacity"
46 | values="0.2;0.5;0.2"
47 | dur="3s"
48 | repeatCount="indefinite" />
49 | </circle>
50 | </pattern>
51 |
52 | <style>
53 | @keyframes floatIn {
54 | 0% {
55 | transform: translateY(20px);
56 | opacity: 0;
57 | }
58 | 100% {
59 | transform: translateY(0);
60 | opacity: 1;
61 | }
62 | }
63 |
64 | @keyframes glowPulse {
65 | 0% { filter: drop-shadow(0 0 2px rgba(255,255,255,0.3)); }
66 | 50% { filter: drop-shadow(0 0 10px rgba(255,255,255,0.5)); }
67 | 100% { filter: drop-shadow(0 0 2px rgba(255,255,255,0.3)); }
68 | }
69 |
70 | @keyframes rotateGlow {
71 | 0% { transform: rotate(0deg) scale(1); opacity: 0.8; }
72 | 50% { transform: rotate(180deg) scale(1.1); opacity: 1; }
73 | 100% { transform: rotate(360deg) scale(1); opacity: 0.8; }
74 | }
75 |
76 | .version-text {
77 | animation: floatIn 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards,
78 | glowPulse 3s ease-in-out infinite;
79 | }
80 |
81 | .date-text {
82 | animation: floatIn 1.2s cubic-bezier(0.4, 0, 0.2, 1) 0.3s forwards;
83 | }
84 |
85 | .decorative-circle {
86 | animation: rotateGlow 15s linear infinite;
87 | }
88 | </style>
89 | </defs>
90 |
91 | <!-- 背景 -->
92 | <rect x="10" y="10" width="780" height="180" rx="30" ry="30"
93 | fill="url(#headerGradient)"
94 | filter="drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1))">
95 | <animate attributeName="filter"
96 | values="drop-shadow(0 4px 6px rgba(0,0,0,0.1));drop-shadow(0 6px 8px rgba(0,0,0,0.2));drop-shadow(0 4px 6px rgba(0,0,0,0.1))"
97 | dur="3s"
98 | repeatCount="indefinite" />
99 | </rect>
100 |
101 | <!-- 装飾パターン -->
102 | <rect x="10" y="10" width="780" height="180" rx="30" ry="30"
103 | fill="url(#dots)" />
104 |
105 | <!-- 装飾的な円 -->
106 | <circle cx="650" cy="100" r="60"
107 | fill="none"
108 | stroke="rgba(255,255,255,0.2)"
109 | stroke-width="2"
110 | class="decorative-circle" />
111 |
112 | <!-- メインコンテンツ -->
113 | <g transform="translate(400,85)" text-anchor="middle">
114 | <text class="version-text"
115 | fill="url(#textGradient)"
116 | font-family="'Segoe UI', system-ui, sans-serif"
117 | font-size="64"
118 | font-weight="bold"
119 | filter="drop-shadow(0 2px 4px rgba(0,0,0,0.2))">
120 | Version 0.2.0
121 | </text>
122 | <text class="date-text"
123 | y="50"
124 | fill="#FFFFFF"
125 | font-family="'Segoe UI', system-ui, sans-serif"
126 | font-size="24"
127 | opacity="0">
128 | GitHub Kanban MCP Server Release Notes
129 | <animate attributeName="fill-opacity"
130 | values="0.7;1;0.7"
131 | dur="3s"
132 | repeatCount="indefinite" />
133 | </text>
134 | </g>
135 |
136 | <!-- 装飾的なアイコン -->
137 | <g transform="translate(80,100)">
138 | <rect x="0" y="0" width="30" height="30" rx="8"
139 | fill="#FFFFFF" opacity="0.9"
140 | transform="rotate(-15)">
141 | <animate attributeName="opacity"
142 | values="0.9;0.7;0.9"
143 | dur="2s"
144 | repeatCount="indefinite" />
145 | </rect>
146 | <rect x="40" y="0" width="30" height="30" rx="8"
147 | fill="#FFFFFF" opacity="0.7"
148 | transform="rotate(15)">
149 | <animate attributeName="opacity"
150 | values="0.7;0.5;0.7"
151 | dur="2s"
152 | repeatCount="indefinite" />
153 | </rect>
154 | <rect x="80" y="0" width="30" height="30" rx="8"
155 | fill="#FFFFFF" opacity="0.5"
156 | transform="rotate(-15)">
157 | <animate attributeName="opacity"
158 | values="0.5;0.3;0.5"
159 | dur="2s"
160 | repeatCount="indefinite" />
161 | </rect>
162 | </g>
163 | </svg>
164 |
```
--------------------------------------------------------------------------------
/assets/header.svg:
--------------------------------------------------------------------------------
```
1 | <?xml version="1.0" encoding="UTF-8"?>
2 | <svg width="800" height="200" viewBox="0 0 800 200" xmlns="http://www.w3.org/2000/svg">
3 | <defs>
4 | <!-- メインの背景グラデーション -->
5 | <linearGradient id="headerGradient" x1="0%" y1="0%" x2="100%" y2="100%">
6 | <stop offset="0%" style="stop-color:#6366f1;stop-opacity:1">
7 | <animate attributeName="stop-color"
8 | values="#6366f1; #8b5cf6; #ec4899; #6366f1"
9 | dur="10s"
10 | repeatCount="indefinite" />
11 | </stop>
12 | <stop offset="50%" style="stop-color:#8b5cf6;stop-opacity:1">
13 | <animate attributeName="stop-color"
14 | values="#8b5cf6; #ec4899; #6366f1; #8b5cf6"
15 | dur="10s"
16 | repeatCount="indefinite" />
17 | </stop>
18 | <stop offset="100%" style="stop-color:#ec4899;stop-opacity:1">
19 | <animate attributeName="stop-color"
20 | values="#ec4899; #6366f1; #8b5cf6; #ec4899"
21 | dur="10s"
22 | repeatCount="indefinite" />
23 | </stop>
24 | </linearGradient>
25 |
26 | <!-- テキストグラデーション -->
27 | <linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
28 | <stop offset="0%" style="stop-color:#ffffff;stop-opacity:1">
29 | <animate attributeName="stop-color"
30 | values="#ffffff; #ffd700; #ffffff"
31 | dur="3s"
32 | repeatCount="indefinite" />
33 | </stop>
34 | <stop offset="100%" style="stop-color:#ffd700;stop-opacity:1">
35 | <animate attributeName="stop-color"
36 | values="#ffd700; #ffffff; #ffd700"
37 | dur="3s"
38 | repeatCount="indefinite" />
39 | </stop>
40 | </linearGradient>
41 |
42 | <!-- 装飾用のパターン -->
43 | <pattern id="dots" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
44 | <circle cx="2" cy="2" r="1" fill="#fff" opacity="0.2">
45 | <animate attributeName="opacity"
46 | values="0.2;0.5;0.2"
47 | dur="3s"
48 | repeatCount="indefinite" />
49 | </circle>
50 | </pattern>
51 |
52 | <style>
53 | @keyframes gradientFlow {
54 | 0% { stop-color: #6366f1; }
55 | 50% { stop-color: #8b5cf6; }
56 | 100% { stop-color: #ec4899; }
57 | }
58 |
59 | @keyframes floatIn {
60 | 0% {
61 | transform: translateY(20px);
62 | opacity: 0;
63 | }
64 | 100% {
65 | transform: translateY(0);
66 | opacity: 1;
67 | }
68 | }
69 |
70 | @keyframes glowPulse {
71 | 0% { filter: drop-shadow(0 0 2px rgba(255,255,255,0.3)); }
72 | 50% { filter: drop-shadow(0 0 10px rgba(255,255,255,0.5)); }
73 | 100% { filter: drop-shadow(0 0 2px rgba(255,255,255,0.3)); }
74 | }
75 |
76 | @keyframes rotateGlow {
77 | 0% { transform: rotate(0deg) scale(1); opacity: 0.8; }
78 | 50% { transform: rotate(180deg) scale(1.1); opacity: 1; }
79 | 100% { transform: rotate(360deg) scale(1); opacity: 0.8; }
80 | }
81 |
82 | @keyframes dashFlow {
83 | to {
84 | stroke-dashoffset: 0;
85 | }
86 | }
87 |
88 | @keyframes shimmer {
89 | 0% { transform: translateX(-100%); }
90 | 100% { transform: translateX(100%); }
91 | }
92 |
93 | .title-text {
94 | animation: floatIn 1.2s cubic-bezier(0.4, 0, 0.2, 1) forwards,
95 | glowPulse 3s ease-in-out infinite;
96 | }
97 |
98 | .subtitle-text {
99 | animation: floatIn 1.2s cubic-bezier(0.4, 0, 0.2, 1) 0.3s forwards;
100 | }
101 |
102 | .decorative-circle {
103 | animation: rotateGlow 15s linear infinite;
104 | }
105 |
106 | .path-animation {
107 | stroke-dasharray: 1000;
108 | stroke-dashoffset: 1000;
109 | animation: dashFlow 2s ease-out forwards;
110 | }
111 |
112 | .shimmer {
113 | animation: shimmer 3s linear infinite;
114 | }
115 | </style>
116 | </defs>
117 |
118 | <!-- 背景 -->
119 | <rect x="10" y="10" width="780" height="180" rx="30" ry="30"
120 | fill="url(#headerGradient)"
121 | filter="drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1))">
122 | <animate attributeName="filter"
123 | values="drop-shadow(0 4px 6px rgba(0,0,0,0.1));drop-shadow(0 6px 8px rgba(0,0,0,0.2));drop-shadow(0 4px 6px rgba(0,0,0,0.1))"
124 | dur="3s"
125 | repeatCount="indefinite" />
126 | </rect>
127 |
128 | <!-- 装飾パターン -->
129 | <rect x="10" y="10" width="780" height="180" rx="30" ry="30"
130 | fill="url(#dots)" />
131 |
132 | <!-- 装飾的な円 -->
133 | <circle cx="650" cy="100" r="60"
134 | fill="none"
135 | stroke="rgba(255,255,255,0.2)"
136 | stroke-width="2"
137 | class="decorative-circle">
138 | <animate attributeName="stroke-width"
139 | values="2;4;2"
140 | dur="3s"
141 | repeatCount="indefinite" />
142 | </circle>
143 |
144 | <!-- 装飾的なライン -->
145 | <path d="M50,100 C150,50 250,150 350,100"
146 | stroke="rgba(255,255,255,0.3)"
147 | stroke-width="2"
148 | fill="none"
149 | class="path-animation">
150 | <animate attributeName="stroke-opacity"
151 | values="0.3;0.6;0.3"
152 | dur="3s"
153 | repeatCount="indefinite" />
154 | </path>
155 |
156 | <!-- メインコンテンツ -->
157 | <g transform="translate(400,85)" text-anchor="middle">
158 | <text class="title-text"
159 | fill="url(#textGradient)"
160 | font-family="'Segoe UI', system-ui, sans-serif"
161 | font-size="48"
162 | font-weight="bold"
163 | filter="drop-shadow(0 2px 4px rgba(0,0,0,0.2))">
164 | GitHub Kanban MCP Server
165 | </text>
166 | <text class="subtitle-text"
167 | y="40"
168 | fill="#FFFFFF"
169 | font-family="'Segoe UI', system-ui, sans-serif"
170 | font-size="20"
171 | opacity="0">
172 | Streamline Your Task Management with LLM
173 | <animate attributeName="fill-opacity"
174 | values="0.7;1;0.7"
175 | dur="3s"
176 | repeatCount="indefinite" />
177 | </text>
178 | </g>
179 |
180 | <!-- 装飾的なアイコン -->
181 | <g transform="translate(80,100)" class="path-animation">
182 | <rect x="0" y="0" width="30" height="30" rx="8"
183 | fill="#FFFFFF" opacity="0.9"
184 | transform="rotate(-15)">
185 | <animate attributeName="opacity"
186 | values="0.9;0.7;0.9"
187 | dur="2s"
188 | repeatCount="indefinite" />
189 | </rect>
190 | <rect x="40" y="0" width="30" height="30" rx="8"
191 | fill="#FFFFFF" opacity="0.7"
192 | transform="rotate(15)">
193 | <animate attributeName="opacity"
194 | values="0.7;0.5;0.7"
195 | dur="2s"
196 | repeatCount="indefinite" />
197 | </rect>
198 | <rect x="80" y="0" width="30" height="30" rx="8"
199 | fill="#FFFFFF" opacity="0.5"
200 | transform="rotate(-15)">
201 | <animate attributeName="opacity"
202 | values="0.5;0.3;0.5"
203 | dur="2s"
204 | repeatCount="indefinite" />
205 | </rect>
206 | </g>
207 | </svg>
208 |
```