hanary-mcp 0.2.1__tar.gz
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.
- hanary_mcp-0.2.1/.gitignore +41 -0
- hanary_mcp-0.2.1/PKG-INFO +103 -0
- hanary_mcp-0.2.1/README.md +84 -0
- hanary_mcp-0.2.1/example.mcp.json +11 -0
- hanary_mcp-0.2.1/pyproject.toml +32 -0
- hanary_mcp-0.2.1/src/hanary_mcp/__init__.py +5 -0
- hanary_mcp-0.2.1/src/hanary_mcp/client.py +163 -0
- hanary_mcp-0.2.1/src/hanary_mcp/server.py +287 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.idea/
|
|
30
|
+
.vscode/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
|
|
34
|
+
# Testing
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
.coverage
|
|
37
|
+
htmlcov/
|
|
38
|
+
|
|
39
|
+
# uv
|
|
40
|
+
.uv/
|
|
41
|
+
uv.lock
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hanary-mcp
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Hanary MCP Server - Task management for Claude Code
|
|
5
|
+
Author: Hanary
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: claude,hanary,mcp,task-management
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: mcp>=1.0.0
|
|
17
|
+
Requires-Dist: requests>=2.32.5
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Hanary MCP Server
|
|
21
|
+
|
|
22
|
+
[Hanary](https://hanary.org) MCP Server for Claude Code - workspace-bound task management.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Using uvx (recommended)
|
|
28
|
+
uvx hanary-mcp --workspace my-project
|
|
29
|
+
|
|
30
|
+
# Or install globally
|
|
31
|
+
uv tool install hanary-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
### Claude Code Setup
|
|
37
|
+
|
|
38
|
+
Add to your project's `.mcp.json`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"hanary": {
|
|
44
|
+
"command": "uvx",
|
|
45
|
+
"args": ["hanary-mcp", "--workspace", "your-workspace-slug"],
|
|
46
|
+
"env": {
|
|
47
|
+
"HANARY_API_TOKEN": "${HANARY_API_TOKEN}"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or add via CLI:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
claude mcp add hanary --transport stdio -- uvx hanary-mcp --workspace your-workspace-slug
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Environment Variables
|
|
61
|
+
|
|
62
|
+
| Variable | Required | Description |
|
|
63
|
+
|----------|----------|-------------|
|
|
64
|
+
| `HANARY_API_TOKEN` | Yes | Your Hanary API token |
|
|
65
|
+
| `HANARY_API_URL` | No | API URL (default: https://hanary.org) |
|
|
66
|
+
|
|
67
|
+
## Available Tools
|
|
68
|
+
|
|
69
|
+
### Task Management
|
|
70
|
+
|
|
71
|
+
- `list_tasks` - List tasks in the workspace
|
|
72
|
+
- `create_task` - Create a new task
|
|
73
|
+
- `update_task` - Update task title/description
|
|
74
|
+
- `complete_task` - Mark task as completed
|
|
75
|
+
- `uncomplete_task` - Mark task as incomplete
|
|
76
|
+
- `delete_task` - Soft delete a task
|
|
77
|
+
- `get_top_task` - Get highest priority incomplete task
|
|
78
|
+
|
|
79
|
+
### Workspace
|
|
80
|
+
|
|
81
|
+
- `get_workspace` - Get workspace details
|
|
82
|
+
- `list_workspace_members` - List workspace members
|
|
83
|
+
|
|
84
|
+
### Messages
|
|
85
|
+
|
|
86
|
+
- `list_messages` - List workspace messages
|
|
87
|
+
- `create_message` - Send a message
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Clone and install
|
|
93
|
+
git clone https://github.com/hanary/hanary-mcp.git
|
|
94
|
+
cd hanary-mcp
|
|
95
|
+
uv sync
|
|
96
|
+
|
|
97
|
+
# Run locally
|
|
98
|
+
HANARY_API_TOKEN=your_token uv run hanary-mcp --workspace test
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Hanary MCP Server
|
|
2
|
+
|
|
3
|
+
[Hanary](https://hanary.org) MCP Server for Claude Code - workspace-bound task management.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Using uvx (recommended)
|
|
9
|
+
uvx hanary-mcp --workspace my-project
|
|
10
|
+
|
|
11
|
+
# Or install globally
|
|
12
|
+
uv tool install hanary-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
### Claude Code Setup
|
|
18
|
+
|
|
19
|
+
Add to your project's `.mcp.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"hanary": {
|
|
25
|
+
"command": "uvx",
|
|
26
|
+
"args": ["hanary-mcp", "--workspace", "your-workspace-slug"],
|
|
27
|
+
"env": {
|
|
28
|
+
"HANARY_API_TOKEN": "${HANARY_API_TOKEN}"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or add via CLI:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
claude mcp add hanary --transport stdio -- uvx hanary-mcp --workspace your-workspace-slug
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Environment Variables
|
|
42
|
+
|
|
43
|
+
| Variable | Required | Description |
|
|
44
|
+
|----------|----------|-------------|
|
|
45
|
+
| `HANARY_API_TOKEN` | Yes | Your Hanary API token |
|
|
46
|
+
| `HANARY_API_URL` | No | API URL (default: https://hanary.org) |
|
|
47
|
+
|
|
48
|
+
## Available Tools
|
|
49
|
+
|
|
50
|
+
### Task Management
|
|
51
|
+
|
|
52
|
+
- `list_tasks` - List tasks in the workspace
|
|
53
|
+
- `create_task` - Create a new task
|
|
54
|
+
- `update_task` - Update task title/description
|
|
55
|
+
- `complete_task` - Mark task as completed
|
|
56
|
+
- `uncomplete_task` - Mark task as incomplete
|
|
57
|
+
- `delete_task` - Soft delete a task
|
|
58
|
+
- `get_top_task` - Get highest priority incomplete task
|
|
59
|
+
|
|
60
|
+
### Workspace
|
|
61
|
+
|
|
62
|
+
- `get_workspace` - Get workspace details
|
|
63
|
+
- `list_workspace_members` - List workspace members
|
|
64
|
+
|
|
65
|
+
### Messages
|
|
66
|
+
|
|
67
|
+
- `list_messages` - List workspace messages
|
|
68
|
+
- `create_message` - Send a message
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Clone and install
|
|
74
|
+
git clone https://github.com/hanary/hanary-mcp.git
|
|
75
|
+
cd hanary-mcp
|
|
76
|
+
uv sync
|
|
77
|
+
|
|
78
|
+
# Run locally
|
|
79
|
+
HANARY_API_TOKEN=your_token uv run hanary-mcp --workspace test
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hanary-mcp"
|
|
3
|
+
version = "0.2.1"
|
|
4
|
+
description = "Hanary MCP Server - Task management for Claude Code"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Hanary" }]
|
|
9
|
+
keywords = ["mcp", "hanary", "claude", "task-management"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.10",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"mcp>=1.0.0",
|
|
21
|
+
"requests>=2.32.5",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
hanary-mcp = "hanary_mcp:main"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/hanary_mcp"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Hanary API Client for MCP Server."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HanaryClient:
|
|
10
|
+
"""Client for Hanary HTTP MCP API."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, api_token: str, api_url: str = "https://hanary.org"):
|
|
13
|
+
self.api_url = api_url.rstrip("/")
|
|
14
|
+
self.api_token = api_token
|
|
15
|
+
self._session: Optional[requests.Session] = None
|
|
16
|
+
self._workspace_id_cache: dict[str, int] = {}
|
|
17
|
+
|
|
18
|
+
def _get_session(self) -> requests.Session:
|
|
19
|
+
if self._session is None:
|
|
20
|
+
self._session = requests.Session()
|
|
21
|
+
self._session.headers.update({
|
|
22
|
+
"Authorization": f"Bearer {self.api_token}",
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
"User-Agent": "curl/8.7.1",
|
|
25
|
+
})
|
|
26
|
+
return self._session
|
|
27
|
+
|
|
28
|
+
async def _call_mcp(self, method: str, params: dict = None) -> dict:
|
|
29
|
+
"""Call the Hanary MCP endpoint."""
|
|
30
|
+
session = self._get_session()
|
|
31
|
+
|
|
32
|
+
request_body = {
|
|
33
|
+
"jsonrpc": "2.0",
|
|
34
|
+
"id": 1,
|
|
35
|
+
"method": method,
|
|
36
|
+
"params": params or {},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
response = session.post(f"{self.api_url}/mcp", json=request_body)
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
|
|
42
|
+
result = response.json()
|
|
43
|
+
|
|
44
|
+
if "error" in result:
|
|
45
|
+
raise Exception(result["error"].get("message", "Unknown error"))
|
|
46
|
+
|
|
47
|
+
return result.get("result", {})
|
|
48
|
+
|
|
49
|
+
async def _get_workspace_id(self, workspace_slug: str) -> int:
|
|
50
|
+
"""Get workspace ID from slug (cached)."""
|
|
51
|
+
if workspace_slug in self._workspace_id_cache:
|
|
52
|
+
return self._workspace_id_cache[workspace_slug]
|
|
53
|
+
|
|
54
|
+
result = await self._call_mcp("tools/call", {
|
|
55
|
+
"name": "get_workspace",
|
|
56
|
+
"arguments": {"slug": workspace_slug}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
content = result.get("content", [])
|
|
60
|
+
if content:
|
|
61
|
+
data = json.loads(content[0].get("text", "{}"))
|
|
62
|
+
workspace = data.get("workspace", {})
|
|
63
|
+
workspace_id = workspace.get("id")
|
|
64
|
+
if workspace_id:
|
|
65
|
+
self._workspace_id_cache[workspace_slug] = workspace_id
|
|
66
|
+
return workspace_id
|
|
67
|
+
|
|
68
|
+
raise Exception(f"Workspace not found: {workspace_slug}")
|
|
69
|
+
|
|
70
|
+
async def _call_tool(self, name: str, arguments: dict) -> str:
|
|
71
|
+
"""Call a tool and return the result as string."""
|
|
72
|
+
result = await self._call_mcp("tools/call", {
|
|
73
|
+
"name": name,
|
|
74
|
+
"arguments": arguments,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
content = result.get("content", [])
|
|
78
|
+
if content:
|
|
79
|
+
return content[0].get("text", "{}")
|
|
80
|
+
return "{}"
|
|
81
|
+
|
|
82
|
+
# Task methods
|
|
83
|
+
async def list_tasks(
|
|
84
|
+
self, workspace_slug: Optional[str] = None, include_completed: bool = False
|
|
85
|
+
) -> str:
|
|
86
|
+
args = {"include_completed": include_completed}
|
|
87
|
+
if workspace_slug:
|
|
88
|
+
workspace_id = await self._get_workspace_id(workspace_slug)
|
|
89
|
+
args["workspace_id"] = str(workspace_id)
|
|
90
|
+
return await self._call_tool("list_tasks", args)
|
|
91
|
+
|
|
92
|
+
async def create_task(
|
|
93
|
+
self,
|
|
94
|
+
title: str,
|
|
95
|
+
workspace_slug: Optional[str] = None,
|
|
96
|
+
description: Optional[str] = None,
|
|
97
|
+
parent_id: Optional[str] = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
args = {"title": title}
|
|
100
|
+
if workspace_slug:
|
|
101
|
+
workspace_id = await self._get_workspace_id(workspace_slug)
|
|
102
|
+
args["workspace_id"] = str(workspace_id)
|
|
103
|
+
if description:
|
|
104
|
+
args["description"] = description
|
|
105
|
+
if parent_id:
|
|
106
|
+
args["parent_id"] = parent_id
|
|
107
|
+
|
|
108
|
+
return await self._call_tool("create_task", args)
|
|
109
|
+
|
|
110
|
+
async def update_task(
|
|
111
|
+
self,
|
|
112
|
+
task_id: str,
|
|
113
|
+
title: Optional[str] = None,
|
|
114
|
+
description: Optional[str] = None,
|
|
115
|
+
) -> str:
|
|
116
|
+
args = {"task_id": task_id}
|
|
117
|
+
if title:
|
|
118
|
+
args["title"] = title
|
|
119
|
+
if description:
|
|
120
|
+
args["description"] = description
|
|
121
|
+
|
|
122
|
+
return await self._call_tool("update_task", args)
|
|
123
|
+
|
|
124
|
+
async def complete_task(self, task_id: str) -> str:
|
|
125
|
+
return await self._call_tool("complete_task", {"task_id": task_id})
|
|
126
|
+
|
|
127
|
+
async def uncomplete_task(self, task_id: str) -> str:
|
|
128
|
+
return await self._call_tool("uncomplete_task", {"task_id": task_id})
|
|
129
|
+
|
|
130
|
+
async def delete_task(self, task_id: str) -> str:
|
|
131
|
+
return await self._call_tool("delete_task", {"task_id": task_id})
|
|
132
|
+
|
|
133
|
+
async def get_top_task(self, workspace_slug: Optional[str] = None) -> str:
|
|
134
|
+
args = {}
|
|
135
|
+
if workspace_slug:
|
|
136
|
+
workspace_id = await self._get_workspace_id(workspace_slug)
|
|
137
|
+
args["workspace_id"] = str(workspace_id)
|
|
138
|
+
return await self._call_tool("get_top_task", args)
|
|
139
|
+
|
|
140
|
+
# Workspace methods
|
|
141
|
+
async def get_workspace(self, workspace_slug: str) -> str:
|
|
142
|
+
return await self._call_tool("get_workspace", {"slug": workspace_slug})
|
|
143
|
+
|
|
144
|
+
async def list_workspace_members(self, workspace_slug: str) -> str:
|
|
145
|
+
workspace_id = await self._get_workspace_id(workspace_slug)
|
|
146
|
+
return await self._call_tool("list_workspace_members", {
|
|
147
|
+
"workspace_id": str(workspace_id),
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
# Message methods
|
|
151
|
+
async def list_messages(self, workspace_slug: str, limit: int = 50) -> str:
|
|
152
|
+
workspace_id = await self._get_workspace_id(workspace_slug)
|
|
153
|
+
return await self._call_tool("list_messages", {
|
|
154
|
+
"workspace_id": str(workspace_id),
|
|
155
|
+
"limit": limit,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
async def create_message(self, workspace_slug: str, content: str) -> str:
|
|
159
|
+
workspace_id = await self._get_workspace_id(workspace_slug)
|
|
160
|
+
return await self._call_tool("create_message", {
|
|
161
|
+
"workspace_id": str(workspace_id),
|
|
162
|
+
"content": content,
|
|
163
|
+
})
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Hanary MCP Server implementation."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from mcp.server import Server
|
|
8
|
+
from mcp.server.stdio import stdio_server
|
|
9
|
+
from mcp.types import Tool, TextContent
|
|
10
|
+
|
|
11
|
+
from .client import HanaryClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_server(workspace: str | None, client: HanaryClient) -> Server:
|
|
15
|
+
"""Create and configure the MCP server."""
|
|
16
|
+
server = Server("hanary")
|
|
17
|
+
|
|
18
|
+
# Determine mode description
|
|
19
|
+
if workspace:
|
|
20
|
+
task_scope = f"workspace '{workspace}'"
|
|
21
|
+
else:
|
|
22
|
+
task_scope = "personal tasks (including assigned workspace tasks)"
|
|
23
|
+
|
|
24
|
+
@server.list_tools()
|
|
25
|
+
async def list_tools() -> list[Tool]:
|
|
26
|
+
tools = [
|
|
27
|
+
Tool(
|
|
28
|
+
name="list_tasks",
|
|
29
|
+
description=f"List tasks for {task_scope}.",
|
|
30
|
+
inputSchema={
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": {
|
|
33
|
+
"include_completed": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"description": "Include completed tasks (default: false)",
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
Tool(
|
|
41
|
+
name="create_task",
|
|
42
|
+
description=f"Create a new task in {task_scope}.",
|
|
43
|
+
inputSchema={
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"title": {"type": "string", "description": "Task title (required)"},
|
|
47
|
+
"description": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "Task description (optional)",
|
|
50
|
+
},
|
|
51
|
+
"parent_id": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Parent task ID for subtask (optional)",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
"required": ["title"],
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
Tool(
|
|
60
|
+
name="update_task",
|
|
61
|
+
description="Update an existing task's title or description.",
|
|
62
|
+
inputSchema={
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {
|
|
65
|
+
"task_id": {"type": "string", "description": "Task ID (required)"},
|
|
66
|
+
"title": {"type": "string", "description": "New title (optional)"},
|
|
67
|
+
"description": {
|
|
68
|
+
"type": "string",
|
|
69
|
+
"description": "New description (optional)",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
"required": ["task_id"],
|
|
73
|
+
},
|
|
74
|
+
),
|
|
75
|
+
Tool(
|
|
76
|
+
name="complete_task",
|
|
77
|
+
description="Mark a task as completed.",
|
|
78
|
+
inputSchema={
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"task_id": {
|
|
82
|
+
"type": "string",
|
|
83
|
+
"description": "Task ID to complete (required)",
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"required": ["task_id"],
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
Tool(
|
|
90
|
+
name="uncomplete_task",
|
|
91
|
+
description="Mark a completed task as incomplete.",
|
|
92
|
+
inputSchema={
|
|
93
|
+
"type": "object",
|
|
94
|
+
"properties": {
|
|
95
|
+
"task_id": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"description": "Task ID to uncomplete (required)",
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"required": ["task_id"],
|
|
101
|
+
},
|
|
102
|
+
),
|
|
103
|
+
Tool(
|
|
104
|
+
name="delete_task",
|
|
105
|
+
description="Soft delete a task.",
|
|
106
|
+
inputSchema={
|
|
107
|
+
"type": "object",
|
|
108
|
+
"properties": {
|
|
109
|
+
"task_id": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"description": "Task ID to delete (required)",
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"required": ["task_id"],
|
|
115
|
+
},
|
|
116
|
+
),
|
|
117
|
+
Tool(
|
|
118
|
+
name="get_top_task",
|
|
119
|
+
description="Get the highest priority incomplete task. Returns the deepest uncompleted task along with its ancestor chain.",
|
|
120
|
+
inputSchema={"type": "object", "properties": {}},
|
|
121
|
+
),
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
# Add workspace-only tools when workspace is specified
|
|
125
|
+
if workspace:
|
|
126
|
+
tools.extend([
|
|
127
|
+
Tool(
|
|
128
|
+
name="get_workspace",
|
|
129
|
+
description="Get details of the current workspace.",
|
|
130
|
+
inputSchema={"type": "object", "properties": {}},
|
|
131
|
+
),
|
|
132
|
+
Tool(
|
|
133
|
+
name="list_workspace_members",
|
|
134
|
+
description="List members of the current workspace.",
|
|
135
|
+
inputSchema={"type": "object", "properties": {}},
|
|
136
|
+
),
|
|
137
|
+
Tool(
|
|
138
|
+
name="list_messages",
|
|
139
|
+
description="List messages in the current workspace.",
|
|
140
|
+
inputSchema={
|
|
141
|
+
"type": "object",
|
|
142
|
+
"properties": {
|
|
143
|
+
"limit": {
|
|
144
|
+
"type": "integer",
|
|
145
|
+
"description": "Number of messages to retrieve (default: 50)",
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
),
|
|
150
|
+
Tool(
|
|
151
|
+
name="create_message",
|
|
152
|
+
description="Send a message to the current workspace.",
|
|
153
|
+
inputSchema={
|
|
154
|
+
"type": "object",
|
|
155
|
+
"properties": {
|
|
156
|
+
"content": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "Message content (required)",
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"required": ["content"],
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
])
|
|
165
|
+
|
|
166
|
+
return tools
|
|
167
|
+
|
|
168
|
+
@server.call_tool()
|
|
169
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
170
|
+
try:
|
|
171
|
+
result = await handle_tool_call(name, arguments, workspace, client)
|
|
172
|
+
return [TextContent(type="text", text=result)]
|
|
173
|
+
except Exception as e:
|
|
174
|
+
return [TextContent(type="text", text=f"Error: {str(e)}")]
|
|
175
|
+
|
|
176
|
+
return server
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def handle_tool_call(
|
|
180
|
+
name: str, arguments: dict, workspace: str | None, client: HanaryClient
|
|
181
|
+
) -> str:
|
|
182
|
+
"""Handle individual tool calls."""
|
|
183
|
+
# Task tools
|
|
184
|
+
if name == "list_tasks":
|
|
185
|
+
return await client.list_tasks(
|
|
186
|
+
workspace_slug=workspace,
|
|
187
|
+
include_completed=arguments.get("include_completed", False),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
elif name == "create_task":
|
|
191
|
+
return await client.create_task(
|
|
192
|
+
title=arguments["title"],
|
|
193
|
+
workspace_slug=workspace,
|
|
194
|
+
description=arguments.get("description"),
|
|
195
|
+
parent_id=arguments.get("parent_id"),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
elif name == "update_task":
|
|
199
|
+
return await client.update_task(
|
|
200
|
+
task_id=arguments["task_id"],
|
|
201
|
+
title=arguments.get("title"),
|
|
202
|
+
description=arguments.get("description"),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
elif name == "complete_task":
|
|
206
|
+
return await client.complete_task(task_id=arguments["task_id"])
|
|
207
|
+
|
|
208
|
+
elif name == "uncomplete_task":
|
|
209
|
+
return await client.uncomplete_task(task_id=arguments["task_id"])
|
|
210
|
+
|
|
211
|
+
elif name == "delete_task":
|
|
212
|
+
return await client.delete_task(task_id=arguments["task_id"])
|
|
213
|
+
|
|
214
|
+
elif name == "get_top_task":
|
|
215
|
+
return await client.get_top_task(workspace_slug=workspace)
|
|
216
|
+
|
|
217
|
+
# Workspace tools
|
|
218
|
+
elif name == "get_workspace":
|
|
219
|
+
return await client.get_workspace(workspace_slug=workspace)
|
|
220
|
+
|
|
221
|
+
elif name == "list_workspace_members":
|
|
222
|
+
return await client.list_workspace_members(workspace_slug=workspace)
|
|
223
|
+
|
|
224
|
+
# Message tools
|
|
225
|
+
elif name == "list_messages":
|
|
226
|
+
return await client.list_messages(
|
|
227
|
+
workspace_slug=workspace,
|
|
228
|
+
limit=arguments.get("limit", 50),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
elif name == "create_message":
|
|
232
|
+
return await client.create_message(
|
|
233
|
+
workspace_slug=workspace,
|
|
234
|
+
content=arguments["content"],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
else:
|
|
238
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def run_server(workspace: str | None, api_token: str, api_url: str):
|
|
242
|
+
"""Run the MCP server."""
|
|
243
|
+
client = HanaryClient(api_token=api_token, api_url=api_url)
|
|
244
|
+
server = create_server(workspace, client)
|
|
245
|
+
|
|
246
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
247
|
+
await server.run(read_stream, write_stream, server.create_initialization_options())
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def main():
|
|
251
|
+
"""Main entry point."""
|
|
252
|
+
parser = argparse.ArgumentParser(
|
|
253
|
+
description="Hanary MCP Server - Task management for Claude Code"
|
|
254
|
+
)
|
|
255
|
+
parser.add_argument(
|
|
256
|
+
"--workspace",
|
|
257
|
+
"-w",
|
|
258
|
+
default=None,
|
|
259
|
+
help="Workspace slug to bind to. If not specified, manages personal tasks.",
|
|
260
|
+
)
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
"--token",
|
|
263
|
+
"-t",
|
|
264
|
+
default=os.environ.get("HANARY_API_TOKEN"),
|
|
265
|
+
help="Hanary API token (or set HANARY_API_TOKEN env var)",
|
|
266
|
+
)
|
|
267
|
+
parser.add_argument(
|
|
268
|
+
"--api-url",
|
|
269
|
+
default=os.environ.get("HANARY_API_URL", "https://hanary.org"),
|
|
270
|
+
help="Hanary API URL (default: https://hanary.org)",
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
args = parser.parse_args()
|
|
274
|
+
|
|
275
|
+
# Get API token from argument or environment
|
|
276
|
+
api_token = args.token
|
|
277
|
+
if not api_token:
|
|
278
|
+
print("Error: --token argument or HANARY_API_TOKEN environment variable is required", file=sys.stderr)
|
|
279
|
+
sys.exit(1)
|
|
280
|
+
|
|
281
|
+
import asyncio
|
|
282
|
+
|
|
283
|
+
asyncio.run(run_server(args.workspace, api_token, args.api_url))
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__":
|
|
287
|
+
main()
|