Skip to content

feat: add MCP issue search - #187

Merged
senamakel merged 3 commits into
mainfrom
mcp-search-issues
Sep 21, 2026
Merged

senamakel merged 3 commits into
mainfrom
mcp-search-issues

Conversation

@senamakel

Copy link
Copy Markdown
Member

Summary

  • expose a read-only search_issues MCP tool over the existing repository-scoped GitHub search
  • return bounded issue metadata and Unicode-safe body excerpts while excluding pull requests
  • document the fourth MCP tool and its default/max result limits

Verification

  • cargo fmt --all -- --check
  • cargo clippy --locked --all-targets -- -D warnings
  • cargo test --locked --features serve server::mcp:: (13 passed)

Co-authored-by: Medulla <medulla@tinyhumans.ai>
@tinysweeper

tinysweeper Bot commented Sep 21, 2026

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 6 lane(s) and found 3 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Incomplete
Priority: high
Reviewed head: 8fb004a0bf7d
Updated: 1790005941 (Unix time)

Review snapshot

Change surface Files Review signal Count
Production 2 Active findings 12
Tests 0 Noted findings 0
Documentation 1 Resolved findings 4
Configuration 0 Pending checks/questions 1

Completeness: Incomplete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

What changed

The review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

  • medium · critique · Count query characters instead of bytes — `str::len()` counts UTF-8 bytes, while GitHub's search limit is expressed in characters. For example, a query containing 200 Japanese characters is 600 bytes but only 200 character (src/forge/github\.rs:1753)
  • medium · tests · Cover the new query construction and error path with a test — The adapter now prepends `is:issue` and enforces a 256‑byte limit on the composed query. Neither behaviour is exercised by any test: the mock forge’s `search_issues` does substring (src/forge/github\.rs:1747)
  • high · e2e · Avoid silently breaking internal callers of search_issues — The `is:issue` qualifier is now appended unconditionally in the shared `search_issues` adapter. Internal callers such as `src/sentry/dedupe.rs` and `src/bin/tinysweeper.rs` (via `b (src/forge/github\.rs:1747)
  • medium · e2e · Cover the 256-byte query rejection end to end — The new 256-byte truncation guard is a behavioural boundary in the forge adapter. The MCP server now exposes a tool that reaches this code, yet no end-to-end test drives a query th (src/forge/github\.rs:1753)

Previously reported and still active

  • Exercise the authenticated issue-search route end to end
  • Exercise create\_issue through the authenticated HTTP route
  • Exercise create\_issue through the authenticated HTTP route
  • Exercise create\_issue through the authenticated HTTP route
  • Exercise create\_issue through the authenticated HTTP route
  • Exercise create\_issue through the authenticated HTTP route
  • Exercise create\_issue through the authenticated HTTP route
  • Exercise create_issue through the authenticated HTTP route

Resolved this pass

  • Preserve the issue's closed state in search results
  • Preserve the issue's closed state in search results
  • Exercise issue search through the production token path
  • Exercise issue search through the production authentication path

Could not review: tinysweeper/description

Before merge

  • Address carried finding Exercise the authenticated issue-search route end to end.
  • Address carried finding Exercise create\_issue through the authenticated HTTP route.
  • Address carried finding Exercise create\_issue through the authenticated HTTP route.
  • Address carried finding Exercise create\_issue through the authenticated HTTP route.
  • Address carried finding Exercise create\_issue through the authenticated HTTP route.
  • Address carried finding Exercise create\_issue through the authenticated HTTP route.
  • Address carried finding Exercise create\_issue through the authenticated HTTP route.
  • Address carried finding Exercise create_issue through the authenticated HTTP route.
  • Complete the description review for tinysweeper/description.
  • Address Avoid silently breaking internal callers of search_issues (src/forge/github\.rs).

How this fits together

flowchart LR
  n0["GitHubRead<br/>changed<br/>4 findings"]:::blocking
  n1["McpState<br/>changed"]:::changed
  n2["build_router<br/>changed"]:::changed
  n3["call<br/>changed"]:::changed
  n4["search_code<br/>changed"]:::changed
  n5["Result"]:::impacted
  n6["new"]:::impacted
  n7["create_issue"]:::impacted
  n8["github"]:::impacted
  n9["search_issues"]:::impacted
  n2 -->|uses| n1
  n2 -->|calls| n6
  n3 -->|uses| n0
  n3 -->|uses| n1
  n3 -->|calls| n4
  n3 -->|uses| n5
  n3 -->|calls| n6
  n3 -->|calls| n7
  n3 -->|uses| n8
  n3 -->|calls| n9
  n4 -->|uses| n1
  n4 -->|uses| n5
  n4 -->|calls| n6
  n6 -->|uses| n5
  n7 -->|uses| n0
  n7 -->|uses| n1
  n7 -->|uses| n5
  n7 -->|calls| n6
  n7 -->|uses| n8
  n7 -->|calls| n9
  n9 -->|uses| n0
  n9 -->|uses| n1
  n9 -->|uses| n5
  n9 -->|calls| n6
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change correctly adds issue-only scoping and rejects oversized queries, but it measures Rust bytes rather than GitHub's documented character limit. Valid non-ASCII searches can therefore be rejected before reaching GitHub; this should be fixed before merging. (11 earlier finding(s) still open) (1 observation(s) grouped into shared inline comments) _The code index is behind this pull request (indexed at `5a157b873912`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._
  • Evidence: src/forge/github\.rs — Count query characters instead of bytes

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The search adapter now explicitly restricts results to issues and enforces GitHub's query-length limit. The change is safe to merge; the earlier closed-state defect is fixed, while the previously requested authenticated search-path tests remain absent. (3 findings discarded for not matching a changed line) (10 earlier finding(s) still open) _The code index is behind this pull request (indexed at `5a157b873912`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: (11 earlier finding(s) still open) (1 observation(s) grouped into shared inline comments) _The code index is behind this pull request (indexed at `5a157b873912`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._
  • Evidence: src/forge/github\.rs — Cover the new query construction and error path with a test

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Neutral
  • Scope reviewed: incomplete; unanswered: tinysweeper/description
  • Lane summary: No reviewer could be consulted.

e2e

  • Conclusion: Failure
  • Scope reviewed: all assigned evidence
  • Lane summary: This revision adds an issue search feature and tightens the issue search query scope, but introduces a regression in the forge adapter by unconditionally appending `is:issue` to the query. The pull request is not safe to merge as-is because existing internal callers of `search_issues` rely on searching both issues and pull requests, and this change silently breaks that contract. (8 earlier finding(s) still open) _The code index is behind this pull request (indexed at `5a157b873912`), so retrieved context may be out of date._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._
  • Evidence: src/forge/github\.rs — Avoid silently breaking internal callers of search_issues
  • Evidence: src/forge/github\.rs — Cover the 256-byte query rejection end to end
Evidence and run details
  • Models: ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash
  • Spend: $0.008953
  • Tokens: 104288 input · 12930 output · 7608 cached · 711 embedding
Head State Pass summary
f024507b51f7 incomplete 3 active finding(s), 2 resolved finding(s) (at 1790004950)
5a157b873912 changes requested 9 active finding(s), 15 resolved finding(s) (at 1790005527)
8fb004a0bf7d incomplete 4 active finding(s), 4 resolved finding(s) (at 1790005941)

tinysweeper 0.1.0

@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 3 billable files and costs up to $0.75.

Or wait 41 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 8aec4a90-d2a6-4702-ba34-776b2eaa4bd5

📥 Commits

Reviewing files that changed from the base of the PR and between 2121d34 and 8fb004a.

📒 Files selected for processing (3)
  • docs/modules/mcp/README.md
  • src/forge/github.rs
  • src/server/mcp.rs

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-21T15:52:49.835178Z 8fb004a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 1 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

             $0.0151 · 220,730 in / 7,314 out · 29,714 cached (13%) · ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash · 384 embedded
critique:    $0.0101 · 143,247 in / 4,456 out · 26,019 cached (18%) · gpt-5.6-luna, deepseek/deepseek-v4-flash
security:    $0.0031 · 44,621 in  / 1,834 out · 3,695 cached (8%)   · gpt-5.6-luna
description: $0.0005 · 8,418 in   / 61 out    · 0 cached (0%)       · deepseek/deepseek-v4-flash
e2e:         $0.0011 · 20,015 in  / 108 out   · 0 cached (0%)       · deepseek/deepseek-v4-flash

Comment thread src/server/mcp.rs
Comment thread src/server/mcp.rs
Comment thread src/server/mcp.rs
@tinysweeper tinysweeper Bot added the priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition. label Sep 21, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f024507b51

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/mcp.rs
Comment thread src/server/mcp.rs
Comment thread src/server/mcp.rs Outdated
Co-authored-by: Medulla <medulla@tinyhumans.ai>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a157b8739

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/forge/github.rs Outdated

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 2 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

             $0.0233 · 347,615 in / 14,142 out · 18,157 cached (5%) · ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash · 668 embedded
critique:    $0.0134 · 198,960 in / 8,244 out  · 12,682 cached (6%) · gpt-5.6-luna, deepseek/deepseek-v4-flash
security:    $0.0063 · 87,352 in  / 4,154 out  · 5,475 cached (6%)  · gpt-5.6-luna
tests:       $0.0011 · 20,125 in  / 121 out    · 0 cached (0%)      · deepseek/deepseek-v4-flash
description: $0.0006 · 10,744 in  / 162 out    · 0 cached (0%)      · deepseek/deepseek-v4-flash
e2e:         $0.0013 · 22,963 in  / 194 out    · 0 cached (0%)      · deepseek/deepseek-v4-flash

Comment thread src/server/mcp.rs
Comment thread src/server/mcp.rs
Comment thread docs/modules/mcp/README.md
Comment thread docs/modules/mcp/README.md
Comment thread src/server/mcp.rs
Comment thread src/forge/github.rs
Comment thread src/server/mcp.rs
@senamakel
senamakel changed the base branch from teeny-mcp to main September 21, 2026 15:46
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@senamakel
senamakel merged commit 39aa4f4 into main Sep 21, 2026
6 checks passed
@senamakel
senamakel deleted the mcp-search-issues branch September 21, 2026 15:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8fb004a0bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/mcp.rs
Comment on lines +299 to +301
if query.split_whitespace().any(|word| {
let word = word.to_ascii_lowercase();
word.starts_with("repo:") || word.starts_with("org:") || word.starts_with("user:")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject parenthesized scope qualifiers

A query such as bug OR (repo:other-org/private-repo parser) passes this check because split_whitespace() produces (repo:other-org/private-repo, which does not start with repo:. GitHub issue search accepts qualifiers inside parenthesized Boolean expressions, so the resulting scoped query can still search another repository and then emit URLs using the requested repository. Fresh evidence in this revision is the unhandled parenthesized form; normalize leading Boolean punctuation or parse and reject all scope qualifiers before searching.

Useful? React with 👍 / 👎.

Comment thread src/forge/github.rs
Comment on lines +1753 to +1755
if scoped.len() > 256 {
return Err(Error::Forge(
"issue search exceeds GitHub's 256-byte query limit".into(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve valid create_issue titles under the search limit

create_issue passes its title to this shared search method before checking force, so adding is:issue plus this 256-byte guard rejects part of the advertised 256-byte title range. For example, a 224-byte ASCII title for tinyhumansai/teeny passed the previous 248-byte scoped query, but the new qualifier makes it 257 bytes and prevents issue creation even with force: true; construct a separately bounded duplicate-search query rather than rejecting an otherwise valid issue title.

Useful? React with 👍 / 👎.

Comment thread src/server/mcp.rs
Comment on lines +278 to +280
let token = read_token(state, repo).await?;
let forge = crate::forge::github::GitHubRead::new(&token).map_err(|error| error.to_string())?;
search_issues_with(&forge, repo, args).await

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse the read client already created for dispatch

Every production search_issues call reaches this line only after call has already invoked read_token and constructed a GitHubRead for canonicalization. Calling read_token again performs another uncached installation_for_repo GitHub request (even when the token itself is cached), adding latency and consuming API capacity on every issue search; pass the existing read client into the handler instead.

Useful? React with 👍 / 👎.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 1 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

          $0.0090 · 104,288 in / 12,930 out · 7,608 cached (7%)  · ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash · 711 embedded
critique: $0.0025 · 32,920 in  / 2,590 out  · 4,048 cached (12%) · gpt-5.6-luna, deepseek/deepseek-v4-flash
security: $0.0020 · 27,236 in  / 1,727 out  · 3,560 cached (13%) · gpt-5.6-luna
tests:    $0.0027 · 17,113 in  / 6,399 out  · 0 cached (0%)      · deepseek/deepseek-v4-flash
e2e:      $0.0012 · 19,948 in  / 732 out    · 0 cached (0%)      · deepseek/deepseek-v4-flash

Comment thread src/forge/github.rs
// Callers exposing GitHub's query language to untrusted input must
// reject scope qualifiers first. The MCP tool does so before reaching
// this shared adapter; internal callers retain the full search syntax.
let scoped = format!(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority high e2e confident

Avoid silently breaking internal callers of search_issues

The is:issue qualifier is now appended unconditionally in the shared search_issues adapter. Internal callers such as src/sentry/dedupe.rs and src/bin/tinysweeper.rs (via backfill_discussions) use search_issues and may rely on it returning both issues and pull requests. Making this change at the lowest shared level without auditing all callers introduces a latent correctness bug for any non-MCP caller that needs PR results. The MCP tool should append is:issue at its own level (in search_issues_with or in a wrapper), not in the forge adapter.


Additional tests observation

priority medium confident

Cover the new query construction and error path with a test

[RULE] untested-behaviour-change

The adapter now prepends is:issue and enforces a 256‑byte limit on the composed query. Neither behaviour is exercised by any test: the mock forge’s search_issues does substring matching without recording or validating the query, and the length‑check branch is never hit. A regression that removed the is:issue qualifier or dropped the length check would not cause a test failure. Add a unit test for GitHubRead::search_issues (or an integration test against a recorded fixture) that verifies the query contains is:issue and that a long query is rejected with the correct error.

Suggested change for this observation (reference only)


[RULE] breaking-contract-change ·

Comment thread src/forge/github.rs
repo.name,
query.trim()
);
if scoped.len() > 256 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium e2e confident

Cover the 256-byte query rejection end to end

The new 256-byte truncation guard is a behavioural boundary in the forge adapter. The MCP server now exposes a tool that reaches this code, yet no end-to-end test drives a query that exceeds 256 bytes. A test that sends a 257-byte query and expects a structured error response would confirm the guard works and that clients see a meaningful error rather than a cryptic failure from GitHub.


Additional critique observation

priority medium confident

Count query characters instead of bytes

[RULE] incorrect-length-unit

str::len() counts UTF-8 bytes, while GitHub's search limit is expressed in characters. For example, a query containing 200 Japanese characters is 600 bytes but only 200 characters, so this adapter rejects a search GitHub accepts. Use a character count for the limit (or otherwise match the API's character-based contract).

Suggested change for this observation (reference only)

if scoped.chars().count() > 256 {

[RULE] blocking-missing-e2e ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant