MCP server for tangled

add MCP client installation instructions and improve auth error messages

- add usage section with client setup for claude code, cursor, codex cli, and generic clients
- improve authentication error messages in _client.py with actionable guidance
- simplify justfile: remove opinionated push/release commands
- document release process in docs/publishing.md
- bump server.json to 0.0.9

+105 -15
+52
README.md
··· 28 29 ## usage 30 31 ```bash 32 uv run tangled-mcp 33 ```
··· 28 29 ## usage 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 + 83 ```bash 84 uv run tangled-mcp 85 ```
+37
docs/publishing.md
··· 47 5. authenticates using GitHub OIDC 48 6. publishes to MCP registry 49 50 ## key learnings 51 52 ### mcp-publisher installation
··· 47 5. authenticates using GitHub OIDC 48 6. publishes to MCP registry 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 + 87 ## key learnings 88 89 ### mcp-publisher installation
-6
justfile
··· 10 # run pre-commit checks 11 check: 12 uv run pre-commit run --all-files 13 - 14 - # push to both tangled and github 15 - push message: 16 - git add . 17 - git commit -m "{{message}}" 18 - git push origin main
··· 10 # run pre-commit checks 11 check: 12 uv run pre-commit run --all-files
+2 -2
server.json
··· 3 "name": "io.github.zzstoatzz/tangled-mcp", 4 "title": "Tangled MCP", 5 "description": "MCP server for Tangled git platform. Manage repositories, branches, and issues on tangled.org.", 6 - "version": "0.0.8", 7 "packages": [ 8 { 9 "registryType": "pypi", 10 "identifier": "tangled-mcp", 11 - "version": "0.0.8", 12 "transport": { 13 "type": "stdio" 14 }
··· 3 "name": "io.github.zzstoatzz/tangled-mcp", 4 "title": "Tangled MCP", 5 "description": "MCP server for Tangled git platform. Manage repositories, branches, and issues on tangled.org.", 6 + "version": "0.0.9", 7 "packages": [ 8 { 9 "registryType": "pypi", 10 "identifier": "tangled-mcp", 11 + "version": "0.0.9", 12 "transport": { 13 "type": "stdio" 14 }
+12 -1
src/tangled_mcp/_tangled/_client.py
··· 74 75 Returns: 76 authenticated client connected to user's PDS 77 """ 78 if settings.tangled_pds_url: 79 client = Client(base_url=settings.tangled_pds_url) 80 else: 81 client = Client() # auto-discover from handle 82 83 - client.login(settings.tangled_handle, settings.tangled_password) 84 return client 85 86
··· 74 75 Returns: 76 authenticated client connected to user's PDS 77 + 78 + Raises: 79 + RuntimeError: if authentication fails (check handle/password) 80 """ 81 if settings.tangled_pds_url: 82 client = Client(base_url=settings.tangled_pds_url) 83 else: 84 client = Client() # auto-discover from handle 85 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 + 95 return client 96 97
+2 -6
src/tangled_mcp/_tangled/_issues.py
··· 51 ) is not None and name == repo_name: 52 repo_at_uri = record.uri 53 # get repo's subscribed labels 54 - if ( 55 - subscribed_labels := getattr(record.value, "labels", None) 56 - ) is not None: 57 repo_labels = subscribed_labels 58 break 59 ··· 161 for record in records.records: 162 if (name := getattr(record.value, "name", None)) and name == repo_name: 163 repo_at_uri = record.uri 164 - if ( 165 - subscribed_labels := getattr(record.value, "labels", None) 166 - ) is not None: 167 repo_labels = subscribed_labels 168 break 169
··· 51 ) is not None and name == repo_name: 52 repo_at_uri = record.uri 53 # get repo's subscribed labels 54 + if (subscribed_labels := getattr(record.value, "labels", None)) is not None: 55 repo_labels = subscribed_labels 56 break 57 ··· 159 for record in records.records: 160 if (name := getattr(record.value, "name", None)) and name == repo_name: 161 repo_at_uri = record.uri 162 + if (subscribed_labels := getattr(record.value, "labels", None)) is not None: 163 repo_labels = subscribed_labels 164 break 165