+52
README.md
+52
README.md
···
28
28
29
29
## usage
30
30
31
+
### using with MCP clients
32
+
33
+
#### claude code
34
+
35
+
```bash
36
+
# basic setup
37
+
claude mcp add tangled -- uvx tangled-mcp
38
+
39
+
# with credentials
40
+
claude mcp add tangled \
41
+
-e TANGLED_HANDLE=your.handle \
42
+
-e TANGLED_PASSWORD=your-app-password \
43
+
-- uvx tangled-mcp
44
+
```
45
+
46
+
#### cursor
47
+
48
+
add to your cursor settings (`~/.cursor/mcp.json` or `.cursor/mcp.json`):
49
+
50
+
```json
51
+
{
52
+
"mcpServers": {
53
+
"tangled": {
54
+
"command": "uvx",
55
+
"args": ["tangled-mcp"],
56
+
"env": {
57
+
"TANGLED_HANDLE": "your.handle",
58
+
"TANGLED_PASSWORD": "your-app-password"
59
+
}
60
+
}
61
+
}
62
+
}
63
+
```
64
+
65
+
#### codex cli
66
+
67
+
```bash
68
+
codex mcp add tangled \
69
+
--env TANGLED_HANDLE=your.handle \
70
+
--env TANGLED_PASSWORD=your-app-password \
71
+
-- uvx tangled-mcp
72
+
```
73
+
74
+
#### other clients
75
+
76
+
for clients that support MCP server configuration, use:
77
+
- **command**: `uvx`
78
+
- **args**: `["tangled-mcp"]`
79
+
- **environment variables**: `TANGLED_HANDLE`, `TANGLED_PASSWORD`, and optionally `TANGLED_PDS_URL`
80
+
81
+
### development usage
82
+
31
83
```bash
32
84
uv run tangled-mcp
33
85
```
+37
docs/publishing.md
+37
docs/publishing.md
···
47
47
5. authenticates using GitHub OIDC
48
48
6. publishes to MCP registry
49
49
50
+
## cutting a release
51
+
52
+
to cut a new release:
53
+
54
+
1. **update server.json version** (both fields must match the version you're releasing)
55
+
```json
56
+
{
57
+
"version": "0.0.9",
58
+
"packages": [{
59
+
"version": "0.0.9"
60
+
}]
61
+
}
62
+
```
63
+
64
+
2. **run pre-commit checks**
65
+
```bash
66
+
just check
67
+
```
68
+
69
+
3. **commit and push your changes**
70
+
```bash
71
+
git add .
72
+
git commit -m "your commit message"
73
+
git push origin main
74
+
```
75
+
76
+
4. **create and push the version tag**
77
+
```bash
78
+
git tag v0.0.9
79
+
git push origin v0.0.9
80
+
```
81
+
82
+
5. **verify the release**
83
+
- workflow: https://github.com/zzstoatzz/tangled-mcp/actions/workflows/publish-mcp.yml
84
+
- pypi: https://pypi.org/project/tangled-mcp/
85
+
- mcp registry: `https://registry.modelcontextprotocol.io/v0/servers/io.github.zzstoatzz%2Ftangled-mcp/versions/X.Y.Z`
86
+
50
87
## key learnings
51
88
52
89
### mcp-publisher installation
-6
justfile
-6
justfile
+2
-2
server.json
+2
-2
server.json
···
3
3
"name": "io.github.zzstoatzz/tangled-mcp",
4
4
"title": "Tangled MCP",
5
5
"description": "MCP server for Tangled git platform. Manage repositories, branches, and issues on tangled.org.",
6
-
"version": "0.0.8",
6
+
"version": "0.0.9",
7
7
"packages": [
8
8
{
9
9
"registryType": "pypi",
10
10
"identifier": "tangled-mcp",
11
-
"version": "0.0.8",
11
+
"version": "0.0.9",
12
12
"transport": {
13
13
"type": "stdio"
14
14
}
+12
-1
src/tangled_mcp/_tangled/_client.py
+12
-1
src/tangled_mcp/_tangled/_client.py
···
74
74
75
75
Returns:
76
76
authenticated client connected to user's PDS
77
+
78
+
Raises:
79
+
RuntimeError: if authentication fails (check handle/password)
77
80
"""
78
81
if settings.tangled_pds_url:
79
82
client = Client(base_url=settings.tangled_pds_url)
80
83
else:
81
84
client = Client() # auto-discover from handle
82
85
83
-
client.login(settings.tangled_handle, settings.tangled_password)
86
+
try:
87
+
client.login(settings.tangled_handle, settings.tangled_password)
88
+
except Exception as e:
89
+
raise RuntimeError(
90
+
f"failed to authenticate with handle '{settings.tangled_handle}'. "
91
+
f"verify TANGLED_HANDLE and TANGLED_PASSWORD are correct. "
92
+
f"error: {e}"
93
+
) from e
94
+
84
95
return client
85
96
86
97
+2
-6
src/tangled_mcp/_tangled/_issues.py
+2
-6
src/tangled_mcp/_tangled/_issues.py
···
51
51
) is not None and name == repo_name:
52
52
repo_at_uri = record.uri
53
53
# get repo's subscribed labels
54
-
if (
55
-
subscribed_labels := getattr(record.value, "labels", None)
56
-
) is not None:
54
+
if (subscribed_labels := getattr(record.value, "labels", None)) is not None:
57
55
repo_labels = subscribed_labels
58
56
break
59
57
···
161
159
for record in records.records:
162
160
if (name := getattr(record.value, "name", None)) and name == repo_name:
163
161
repo_at_uri = record.uri
164
-
if (
165
-
subscribed_labels := getattr(record.value, "labels", None)
166
-
) is not None:
162
+
if (subscribed_labels := getattr(record.value, "labels", None)) is not None:
167
163
repo_labels = subscribed_labels
168
164
break
169
165