MCP server for tangled

fix issue creation: use repo AT-URI, sequential issueId, and owner field

+79 -9
+79 -9
src/tangled_mcp/_tangled/_client.py
··· 163 163 return make_tangled_request("sh.tangled.repo.branches", params, knot=knot) 164 164 165 165 166 - def create_issue(repo: str, title: str, body: str | None = None) -> dict[str, Any]: 166 + def create_issue(repo_id: str, title: str, body: str | None = None) -> dict[str, Any]: 167 167 """create an issue on a repository 168 168 169 169 Args: 170 - repo: repository AT-URI (e.g., 'at://did:plc:.../sh.tangled.repo.repo/...') 170 + repo_id: repository identifier in "did/repo" format (e.g., 'did:plc:.../tangled-mcp') 171 171 title: issue title 172 172 body: optional issue body/description 173 173 ··· 179 179 if not client.me: 180 180 raise RuntimeError("client not authenticated") 181 181 182 + # parse repo_id to get owner_did and repo_name 183 + if "/" not in repo_id: 184 + raise ValueError(f"invalid repo_id format: {repo_id}") 185 + 186 + owner_did, repo_name = repo_id.split("/", 1) 187 + 188 + # get the repo AT-URI by querying the repo collection 189 + records = client.com.atproto.repo.list_records( 190 + models.ComAtprotoRepoListRecords.Params( 191 + repo=owner_did, 192 + collection="sh.tangled.repo", 193 + limit=100, 194 + ) 195 + ) 196 + 197 + repo_at_uri = None 198 + for record in records.records: 199 + if hasattr(record.value, "name") and record.value.name == repo_name: 200 + repo_at_uri = record.uri 201 + break 202 + 203 + if not repo_at_uri: 204 + raise ValueError(f"repo not found: {repo_id}") 205 + 206 + # query existing issues to determine next issueId 207 + existing_issues = client.com.atproto.repo.list_records( 208 + models.ComAtprotoRepoListRecords.Params( 209 + repo=client.me.did, 210 + collection="sh.tangled.repo.issue", 211 + limit=100, 212 + ) 213 + ) 214 + 215 + # find max issueId for this repo 216 + max_issue_id = 0 217 + for issue_record in existing_issues.records: 218 + if hasattr(issue_record.value, "repo") and issue_record.value.repo == repo_at_uri: 219 + issue_id = getattr(issue_record.value, "issueId", None) 220 + if issue_id is not None: 221 + max_issue_id = max(max_issue_id, issue_id) 222 + 223 + next_issue_id = max_issue_id + 1 224 + 182 225 # generate timestamp ID for rkey 183 226 tid = int(datetime.now(timezone.utc).timestamp() * 1000000) 184 227 rkey = str(tid) 185 228 186 - # create issue record 229 + # create issue record with proper schema 187 230 record = { 188 231 "$type": "sh.tangled.repo.issue", 189 - "repo": repo, 232 + "repo": repo_at_uri, # full AT-URI of repo record 233 + "issueId": next_issue_id, # sequential issue ID 234 + "owner": client.me.did, # issue creator's DID 190 235 "title": title, 191 236 "body": body, 192 237 "createdAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), ··· 202 247 ) 203 248 ) 204 249 205 - return {"uri": response.uri, "cid": response.cid} 250 + return {"uri": response.uri, "cid": response.cid, "issueId": next_issue_id} 206 251 207 252 208 253 def list_repo_issues( 209 - repo: str, limit: int = 50, cursor: str | None = None 254 + repo_id: str, limit: int = 50, cursor: str | None = None 210 255 ) -> dict[str, Any]: 211 256 """list issues for a repository 212 257 213 258 Args: 214 - repo: repository AT-URI 259 + repo_id: repository identifier in "did/repo" format 215 260 limit: maximum number of issues to return 216 261 cursor: pagination cursor 217 262 ··· 223 268 if not client.me: 224 269 raise RuntimeError("client not authenticated") 225 270 271 + # parse repo_id to get owner_did and repo_name 272 + if "/" not in repo_id: 273 + raise ValueError(f"invalid repo_id format: {repo_id}") 274 + 275 + owner_did, repo_name = repo_id.split("/", 1) 276 + 277 + # get the repo AT-URI by querying the repo collection 278 + records = client.com.atproto.repo.list_records( 279 + models.ComAtprotoRepoListRecords.Params( 280 + repo=owner_did, 281 + collection="sh.tangled.repo", 282 + limit=100, 283 + ) 284 + ) 285 + 286 + repo_at_uri = None 287 + for record in records.records: 288 + if hasattr(record.value, "name") and record.value.name == repo_name: 289 + repo_at_uri = record.uri 290 + break 291 + 292 + if not repo_at_uri: 293 + raise ValueError(f"repo not found: {repo_id}") 294 + 226 295 # list records from the issue collection 227 296 response = client.com.atproto.repo.list_records( 228 297 models.ComAtprotoRepoListRecords.Params( ··· 236 305 # filter issues by repo 237 306 issues = [] 238 307 for record in response.records: 239 - if hasattr(record.value, "repo") and record.value.repo == repo: 308 + if hasattr(record.value, "repo") and record.value.repo == repo_at_uri: 240 309 issues.append( 241 310 { 242 311 "uri": record.uri, 243 312 "cid": record.cid, 313 + "issueId": getattr(record.value, "issueId", 0), 244 314 "title": getattr(record.value, "title", ""), 245 315 "body": getattr(record.value, "body", None), 246 - "createdAt": getattr(record.value, "created_at", ""), 316 + "createdAt": getattr(record.value, "createdAt", ""), 247 317 } 248 318 ) 249 319