diff --git a/api/mcp/__init__.py b/api/mcp/__init__.py new file mode 100644 index 00000000..c161c8e1 --- /dev/null +++ b/api/mcp/__init__.py @@ -0,0 +1,8 @@ +"""MCP server module for code-graph. + +Exposes the code-graph indexer and graph queries as MCP tools so AI coding +agents (Claude Code, Cursor, Copilot, Roo/Cline) can drive the indexer over +the standard Model Context Protocol stdio transport. + +Entry point: ``cgraph-mcp`` (defined in ``pyproject.toml``). +""" diff --git a/api/mcp/server.py b/api/mcp/server.py new file mode 100644 index 00000000..d81111df --- /dev/null +++ b/api/mcp/server.py @@ -0,0 +1,26 @@ +"""FastMCP server for code-graph. + +This is the scaffold (T1). It instantiates a single FastMCP app, exposes it +as ``app`` for tests and embedders, and registers a ``main()`` entry point +that runs the server over stdio. Tools are registered in later tickets +(T4-T8, T11) by importing this module's ``app`` and decorating functions +with ``@app.tool(...)``. +""" + +from __future__ import annotations + +from mcp.server.fastmcp import FastMCP + +app: FastMCP = FastMCP("code-graph") + + +def main() -> None: + """Run the MCP server over stdio. + + Console-script entry point for ``cgraph-mcp``. + """ + app.run() + + +if __name__ == "__main__": + main() diff --git a/docs/MCP_SERVER_DESIGN.md b/docs/MCP_SERVER_DESIGN.md new file mode 100644 index 00000000..38b2f077 --- /dev/null +++ b/docs/MCP_SERVER_DESIGN.md @@ -0,0 +1,321 @@ +# Code Graph MCP Server — Design Summary + +## What We're Building + +An MCP server inside the `code-graph` repo that gives AI coding agents two capabilities: +1. **Structural tools** (deterministic, no LLM): get_callers, get_callees, get_dependencies, impact_analysis, find_path, search_code, index_repo +2. **Ask tool** (GraphRAG SDK, needs LLM): natural language questions about the codebase → NL-to-Cypher → grounded answers + +Phase 1 also bundles three foundational improvements to `api/` that the MCP server needs and that benefit all consumers of code-graph (CLI, web UI, future integrations): + +3. **Multi-branch graph identity**: per `(repo, branch)` graph naming so concurrent agents on different branches can't corrupt each other's view. (Today, two users on different branches overwrite each other's graphs — this is a real bug.) +4. **Incremental indexing**: file-hash-based skip-unchanged so agents can call `index_repo` cheaply on every interaction. Default-on once a graph exists. +5. **Tree-sitter language expansion**: a shared `TreeSitterAnalyzer` base class plus 5 new languages (Go, Rust, TypeScript, Ruby, C++) and re-enabling C. Brings supported languages from 5 to 11. + +## Key Decisions Made + +- **Mono-repo**: Build inside `code-graph/api/mcp/`, not a separate project. One pip package, one repo. +- **Module path is `api/mcp/`, NOT top-level `mcp/`**: A top-level `mcp/` directory would shadow the installed `mcp` PyPI SDK and break `from mcp.server.fastmcp import FastMCP`. Entry point: `cgraph-mcp = "api.mcp.server:main"`. +- **Python MCP server**: Use the official `mcp` Python SDK (`from mcp.server.fastmcp import FastMCP`), NOT standalone `fastmcp` or Node.js. Avoids language bridge. +- **Reuse everything**: Most code already exists. The MCP tools are thin wrappers around `api/graph.py`, `api/project.py`, `api/cli.py`, and `api/llm.py`. +- **GraphRAG SDK powers the ask tool**: `kg.ask()` does NL-to-Cypher. Code-graph's `api/llm.py` already integrates this — repackage for MCP. +- **Reuse the existing hand-coded ontology** from `api/llm.py:_define_ontology()` (lines 26–233) rather than auto-extracting via `Ontology.from_kg_graph()`. The hand-coded version has richer entity attributes and descriptions tuned for code. Refactor: rename `_define_ontology` → `define_ontology` so the MCP module can import it. +- **11 languages in Phase 1**: Python/JS/Kotlin via tree-sitter (refactored onto a shared base class in T15), Go/Rust/TypeScript/Ruby/C/C++ added in T16, Java/C# stay on multilspy. +- **Incremental indexing in Phase 1**: file-hash-based skip-unchanged, default-on once a `(project, branch)` graph exists (T18). Full re-index via `--full` (CLI) or `incremental=False` (MCP). +- **No Graphiti/memory or raw FalkorDB MCP in v1**: Out of scope. Available as separate servers. Architecture supports merging later. +- **Auto-init for zero config**: ensure-db auto-starts FalkorDB Docker, auto-index on first tool call, auto-GraphRAG init. +- **Expose Cypher in ask responses**: Transparency for the agent + learning patterns. +- **Stdio transport only in Phase 1**: HTTP/SSE deferred to Phase 1.5. Stdio is sufficient for Claude Code, Cursor, and Claude Desktop. +- **`impact_analysis` defaults**: direction `IN` (upstream callers — "what breaks if I change this"), depth `3`, max depth clamp `10`. Parameters allow override. +- **Multi-branch graph identity**: graph name is `code:{project}:{branch}`. Sourcegraph-zoekt-style isolation. The IDE/LSP "single current index, re-index on switch" model only works because IDEs are single-tenant; code-graph is multi-tenant (a server, not a desktop tool, with concurrent agents on different branches), so per-branch isolation is required for correctness. Existing single-graph deployments migrate to `code:{project}:_default`. Within a branch, the existing transition-based `switch_commit` flow continues to work unchanged. +- **`index_repo` auto-detects branch**: from `git rev-parse --abbrev-ref HEAD` in the target path. Optional override parameter. Non-git paths use `_default`. +- **Incremental indexing default-on**: once a `(project, branch)` graph exists, `index_repo` and `cgraph index` auto-detect and run incremental. Opt out with `--full` (CLI) or `incremental=False` (MCP). First-time runs are full. Tracks per-file SHA256 hashes in Redis under `{repo}:{branch}_files`. Built on the existing `delete_files()` primitive in `api/graph.py` (today only used in the git-history flow). +- **Tree-sitter base class**: refactor existing PythonAnalyzer / JavaScriptAnalyzer / KotlinAnalyzer onto a shared `TreeSitterAnalyzer` base in Phase 1. Five new languages added on the new base in the same phase. C is re-enabled. Java and C# stay on multilspy (LSP) until a future phase. + +## Directory Structure + +```text +code-graph/ +├── api/ # Existing Python backend (FastAPI, analyzers, graph, llm, cli) +│ └── mcp/ # NEW — MCP server module (under api/ to avoid shadowing the installed `mcp` SDK) +│ ├── __init__.py +│ ├── server.py # FastMCP entry point, tool registration, stdio transport +│ ├── auto_init.py # ensure-db + auto-index hooks +│ ├── graphrag_init.py # KnowledgeGraph construction + per-project caching (reuses api/llm.py:define_ontology) +│ ├── code_prompts.py # Re-exports + hooks for GraphRAG prompts (sourced from api/prompts.py) +│ ├── templates/ # Agent guidance file templates (cursorrules, claude_mcp_section) +│ └── tools/ +│ ├── __init__.py +│ ├── structural.py # index_repo, get_callers, get_callees, get_dependencies, +│ │ # impact_analysis, find_path, search_code +│ └── ask.py # GraphRAG-powered ask tool +├── app/ # Existing React frontend +├── skills/ # Existing Claude Code skill +├── tests/ +│ └── mcp/ +│ ├── __init__.py +│ ├── conftest.py # Session-scoped FalkorDB + indexed-fixture fixtures +│ ├── fixtures/ +│ │ ├── sample_project/ # Py/Java/C# fixture with known call graph +│ │ └── expected.yaml # Assertion contract: counts, callers, callees, paths, search hits +│ ├── test_scaffold.py # Scaffold smoke test (T1) +│ ├── test_index_repo.py # T4 — unit + integration + protocol +│ ├── test_neighbors.py # T5 — unit + integration + protocol + CLI parity +│ ├── test_impact_analysis.py # T6 +│ ├── test_find_path.py # T7 +│ ├── test_search_code.py # T8 +│ ├── test_graphrag_init.py # T9 +│ ├── test_code_prompts.py # T10 (snapshot) +│ ├── test_ask.py # T11 — mocked LLM, real Cypher against fixture +│ ├── test_auto_init.py # T12 +│ └── test_init_agent.py # T13 +└── pyproject.toml # Adds `cgraph-mcp = "api.mcp.server:main"` and `mcp>=1.0,<2.0` +``` + +**Note on test layout:** Each tool ticket ships its own integration + MCP-protocol round-trip tests in the same PR — there is no separate "integration tests" or "protocol tests" milestone. The previous `integration/` and `e2e/` subdirectories are removed in favor of per-tool test files. Real-LLM E2E is deferred to Phase 1.5. + +## CLI-to-MCP Tool Mapping + +| cgraph Command | MCP Tool | Shared Code | Delta | +|---|---|---|---| +| `cgraph index` / `index-repo` | `index_repo` | api/project.py, analyzers/ | + GraphRAG init after indexing | +| `cgraph neighbors --rel CALLS --dir in` | `get_callers` | api/graph.py Cypher | Thin wrapper | +| `cgraph neighbors --rel CALLS --dir out` | `get_callees` | api/graph.py Cypher | Thin wrapper | +| `cgraph neighbors` (multi-rel) | `get_dependencies` | api/graph.py Cypher | New multi-rel query | +| (new) | `impact_analysis` | api/graph.py Cypher | New variable-depth traversal | +| `cgraph paths` | `find_path` | api/graph.py Cypher | Thin wrapper | +| `cgraph search` | `search_code` | api/auto_complete.py | Thin wrapper | +| (web UI chat) | `ask` | api/llm.py + GraphRAG SDK | Repackage as MCP tool | + +## GraphRAG SDK Integration Pattern + +The MCP `ask` tool is a thin wrapper around the GraphRAG SDK flow already implemented in `api/llm.py`. End-to-end: + +1. **Pre-step (once per project, in `api/mcp/graphrag_init.py`):** construct a `KnowledgeGraph` and cache it. Reuses the existing hand-coded ontology from `api/llm.py:define_ontology()` (renamed from `_define_ontology` in T9). + +2. **Per-call (in the `ask` tool):** retrieve cached `KnowledgeGraph`, call `kg.ask(question)`, return `{answer, cypher_query, context_nodes}`. Internally this is **two LLM round-trips bracketing one Cypher query against FalkorDB**: + - LLM #1: question + ontology → Cypher + - FalkorDB: execute Cypher → rows + - LLM #2: question + rows → natural-language answer + + The graph itself never goes to the LLM — only schema and query results — which is why this works on huge codebases. + +```python +# api/mcp/graphrag_init.py — construct once, cache per project +from graphrag_sdk import KnowledgeGraph +from graphrag_sdk.models.litellm import LiteModel +from graphrag_sdk.model_config import KnowledgeGraphModelConfig + +from api.llm import define_ontology # renamed from _define_ontology in T9 +from api.mcp.code_prompts import ( + CYPHER_GEN_SYSTEM, CYPHER_GEN_PROMPT, + GRAPH_QA_SYSTEM, GRAPH_QA_PROMPT, +) + +_kg_cache: dict[str, KnowledgeGraph] = {} + +def get_or_create_kg(project_name: str) -> KnowledgeGraph: + if project_name in _kg_cache: + return _kg_cache[project_name] + + model = LiteModel(model_name=os.getenv("MODEL_NAME", "gemini/gemini-flash-lite-latest")) + kg = KnowledgeGraph( + name=f"code:{project_name}", + model_config=KnowledgeGraphModelConfig.with_model(model), + ontology=define_ontology(), # REUSE — do NOT call Ontology.from_kg_graph + cypher_system_instruction=CYPHER_GEN_SYSTEM, + qa_system_instruction=GRAPH_QA_SYSTEM, + cypher_gen_prompt=CYPHER_GEN_PROMPT, + qa_prompt=GRAPH_QA_PROMPT, + ) + _kg_cache[project_name] = kg + return kg + +# api/mcp/tools/ask.py — the tool itself +async def ask(question: str) -> dict: + kg = get_or_create_kg(current_project_name()) + response = await asyncio.get_event_loop().run_in_executor(None, kg.ask, question) + return { + "answer": response.answer, + "cypher_query": response.cypher_query, # exposed for transparency + "context_nodes": response.context_nodes, + } +``` + +## Multi-Branch Graph Identity + +**Problem.** Today, `Graph(project_name)` (api/project.py:225, api/index.py:246) names every graph after the repo directory. There is no branch component. Two users (or agents) indexing the same repo on different branches **silently overwrite each other** — the second indexer wipes the first. The Redis metadata under `{repo}_info` (api/info.py:33-46) is also a single global hash per repo, so the commit pointer is shared. This is a real bug that the MCP server will hit immediately because agents working on PR branches need their branch's view, not stale state from someone else's main. + +**Comparison with prior art.** + +| System | Storage | Branch behavior | Why it works for them | +|---|---|---|---| +| JetBrains IDEs | Single on-disk index | Filesystem-state-based; re-index on `git checkout` | Single-tenant; one developer, one workspace | +| VS Code + LSPs | In-memory per workspace + content-hash cache | LSP receives `didChangeWatchedFiles`, reanalyzes touched files | Single-tenant; each workspace is isolated by process | +| GitHub Blackbird | Server-side, commit-sharded, deduped | All commits indexed simultaneously | Massive scale; full historical search | +| Sourcegraph zoekt | Per `(repo, branch)` shard | Default branch always indexed; others by config | Server, multi-tenant, interactive use | +| Sourcegraph SCIP | Per-commit graph data uploaded by CI | Commit-keyed | Reproducible cross-commit code intel | + +Code-graph is architecturally a server (FalkorDB-backed, accessed by multiple clients), so the IDE single-index model is wrong for it. The closest analog is **Sourcegraph zoekt: per `(repo, branch)` graphs**. + +**Solution (T17).** Graph naming becomes `code:{project_name}:{branch}`. The full set of changes: + +| File | Change | +|---|---| +| `api/graph.py` | `Graph` and `AsyncGraphQuery` constructors take a `branch` parameter; default `_default`; graph name composed as `code:{project}:{branch}` | +| `api/project.py` | `Project.from_git_repository()` and `Project.from_local_directory()` accept and propagate `branch`; auto-detect via `git rev-parse --abbrev-ref HEAD` when `None` | +| `api/info.py` | Redis metadata keys become `{repo}:{branch}_info`; `set_repo_commit`, `get_repo_commit`, etc. take a branch param | +| `api/git_utils/` | Git-transitions graph becomes `{repo}:{branch}_git`; `switch_commit` stays scoped to a single branch graph | +| `api/cli.py` | `cgraph index --branch `; `cgraph list` enumerates `(project, branch)` pairs; `cgraph info`, `cgraph search` accept `--branch` | +| `api/index.py` | `/api/list_repos`, `/api/graph_entities`, `/api/repo_info`, `/api/get_neighbors`, `/api/find_paths`, `/api/auto_complete` accept optional `branch` query param; responses include the branch | +| MCP `index_repo` | Accepts optional `branch`; auto-detects from target path's checkout; returns `branch` in response | + +**Migration.** A one-shot helper renames `code:{project}` → `code:{project}:_default` and copies `{repo}_info` → `{repo}:_default_info` on first read. Documented as `cgraph migrate` for explicit invocation. + +**Out of scope for Phase 1.** Cross-branch query tools, branch comparison, branch garbage collection. Each branch is an isolated graph; users prune via `cgraph delete --branch`. + +## Incremental Indexing + +**Today.** `SourceAnalyzer.first_pass()` (`api/analyzers/source_analyzer.py:82-121`) re-parses **every supported file** on every index call. There is no per-file hash tracking. The codebase already has the primitives for incremental work — `Graph.delete_files()` (referenced in `api/git_utils/git_utils.py:153, 217`) and the file-classification logic in `classify_changes` — but they're coupled to the git-history-build flow and not used for ad-hoc reindexing. + +**Solution (T18).** Add file-hash-based incremental indexing on top of T17's per-branch storage. Flow: + +1. **Hash store**: Redis hash `{repo}:{branch}_files` mapping `file_path → SHA256(content)`. Persisted at the end of every full or incremental index. +2. **Diff phase**: `Project.analyze_sources(incremental=True)` walks the file tree, computes current hashes, diffs against the stored map: + - **Unchanged** → skip the analyzer entirely + - **Modified** → call `delete_files([path])` to remove old graph entities, then re-run the analyzer (first pass) on the file + - **Deleted** → call `delete_files([path])` only + - **New** → analyze normally +3. **Second-pass (LSP) decision**: if any file changed, run the LSP-based second pass over the entire branch graph. Per-file second-pass is a Phase 2 optimization (correctness over speed in v1). +4. **Persist** the new hash map. + +**Defaults.** +- First run on a fresh `(project, branch)` → automatically falls back to full +- Hash store missing or corrupted → falls back to full with a warning logged +- File renames → treated as delete + add (rename detection deferred to Phase 2) +- CLI `cgraph index .` defaults to incremental when a graph exists; `--full` forces full +- MCP `index_repo` defaults to incremental; response includes `mode: "full"|"incremental"` and `files_changed: list[str]` + +**Why this is the right primitive for agents.** Agents call `index_repo` reflexively at the start of every interaction to ensure freshness. With full re-index, this is too slow to be reflexive. With incremental + content-hash skipping (the same trick LSP servers use), the steady-state cost is "diff a few file hashes" — acceptable for every-call use. + +## Competitive Context + +5 competitors exist: codebase-memory-mcp (66 langs, SQLite), GitNexus (23.8K stars, KuzuDB), Codegraph (11 langs, 30+ tools, SQLite), CodeGraphContext (Neo4j), Code Pathfinder (Python only). + +**All share 3 gaps FalkorDB fills:** +1. No NL query layer (none have an "ask" tool — GraphRAG SDK is the differentiator) +2. Local-only embedded storage (all SQLite — FalkorDB is client-server, supports shared/team/cloud) +3. No ecosystem path to memory/intelligence (FalkorDB has Graphiti, GraphRAG SDK, mcpserver) + +**Don't compete on:** tool count or language count. +**Compete on:** ask tool (understanding), shared graphs (scale), cloud path (enterprise). + +## CI Testing Strategy + +**Each tool ticket ships its own four kinds of tests in the same PR.** No bulk-testing milestones at the end. + +| Layer | Runs On | FalkorDB | LLM | What It Tests | +|---|---|---|---|---| +| Unit tests | Every PR | Mocked | No | Parameter parsing, Cypher generation, output formatting, error handling | +| Integration (structural) | Every PR | Docker service | No | Tool against indexed fixture, asserted via `expected.yaml` contract | +| Integration (ask, mocked) | Every PR | Docker service | Mocked | GraphRAG init, prompt construction, real Cypher execution against fixture, answer formatting | +| MCP protocol round-trip | Every PR | Docker service | No | `session.list_tools()` schema check + `session.call_tool(...)` round-trip via the `mcp` SDK's stdio client | +| CLI parity (where applicable) | Every PR | Docker service | No | MCP tool output matches the equivalent `cgraph` CLI command output | +| E2E (ask, real LLM) | **Phase 1.5** (nightly, deferred) | Docker service | Real (secret) | Prompt quality, answer grounding, regression detection | + +GitHub Actions FalkorDB service (added in T2): +```yaml +services: + falkordb: + image: falkordb/falkordb:latest + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 +``` + +## GitHub Issues Breakdown (Phase 1: 18 vertical issues) + +Each tool ticket ships impl + unit + integration + protocol round-trip in a single PR. There are no separate "testing" milestones — testing is folded into every ticket. + +### Foundation +1. **T1 — Scaffold `api/mcp/` module + `cgraph-mcp` entry point.** FastMCP server, stdio runner, `mcp>=1.0,<2.0` dep, copy design doc into repo, scaffold smoke test. +2. **T2 — CI workflow with FalkorDB service.** `.github/workflows/mcp-tests.yml`, FalkorDB service container, runs on path filter, scaffold smoke test green. +3. **T3 — Test fixture project + assertion contract.** `tests/mcp/fixtures/sample_project/` with known call graph in Py/Java/C#, plus `expected.yaml` and session-scoped `conftest.py`. + +### Core `api/` improvements (prerequisite to MCP tools) +17. **T17 — Per-branch graph identity.** `code:{project}:{branch}` naming everywhere; branch param on `Graph`, `Project`, `info`, CLI, REST endpoints; one-shot migration helper to `_default`. **On the critical path before T4.** +18. **T18 — Incremental indexing.** File-hash-based skip-unchanged in `SourceAnalyzer`, default-on once a graph exists. Builds on T17. CLI `--full` flag; MCP `incremental` parameter. + +### Structural Tools (each ticket: impl + unit + integration + protocol round-trip + CLI parity) +4. **T4 — `index_repo` tool.** Wraps `Project.from_git_repository` + `analyze_sources` post-T17. Auto-detects branch; supports incremental via T18. +5. **T5 — `get_callers` / `get_callees` / `get_dependencies` tools.** Three tools sharing one helper over `AsyncGraphQuery.get_neighbors`. +6. **T6 — `impact_analysis` tool.** New variable-depth Cypher in `api/graph.py`. Defaults: direction `IN`, depth `3`, max clamp `10`. +7. **T7 — `find_path` tool.** Wraps `AsyncGraphQuery.find_paths`. +8. **T8 — `search_code` tool.** Wraps `AsyncGraphQuery.prefix_search`. + +### Ask Tool + GraphRAG +9. **T9 — GraphRAG init module (`api/mcp/graphrag_init.py`).** Reuses `api/llm.py:define_ontology` (renamed from `_define_ontology`). Caches `KnowledgeGraph` per `(project, branch)`. +10. **T10 — Code-specific prompt overrides (`api/mcp/code_prompts.py`).** Re-exports + snapshot-pinned prompts from `api/prompts.py`. +11. **T11 — `ask` MCP tool.** Returns `{answer, cypher_query, context_nodes}`. Mocked-LLM integration test executes real Cypher against the T3 fixture. + +### Operational +12. **T12 — Auto-init: ensure FalkorDB + auto-index CWD.** Bootstraps Docker if FalkorDB unreachable; lazy auto-index gated on `CODE_GRAPH_AUTO_INDEX=true`. +13. **T13 — Agent guidance bundle.** AGENTS.md section, `.cursorrules` template, `cgraph init-agent` CLI command. +14. **T14 — Packaging.** Dockerfile MCP mode, docker-compose for FalkorDB + MCP server, README quickstart with `claude mcp add-json` snippet. + +### Tree-sitter expansion (parallel swimlane) +15. **T15 — Tree-sitter analyzer base class refactor.** Extract `TreeSitterAnalyzer` base from existing Python/JS/Kotlin analyzers. Strictly non-functional. +16. **T16 — Add 5 new tree-sitter languages + re-enable C.** Go, Rust, TypeScript, Ruby, C++; per-language fixtures and tests. Brings supported languages from 5 to 11. + +### Deferred to Phase 1.5 +- HTTP/SSE transport (was Phase 1 issue #3 in earlier draft) +- Real-LLM nightly E2E with API-key secrets (was a row in the CI table) + +### Dependency graph +```text +T1 ──┬─> T2 ──> T3 ──> T17 ──> T4 ──┬─> T5 + │ ├─> T6 + │ ├─> T7 + │ └─> T8 + ├─> T9 ──> T10 ──> T11 (also needs T3, T17) + ├─> T12 (also needs T4) + ├─> T13 + ├─> T14 (also needs T12) + ├─> T15 ──> T16 + └─> T18 (also needs T17, lands in parallel with T4+) +``` +After T17 lands, multiple streams parallelize: structural tools (T4 → T5/T6/T7/T8), ask (T9 → T10 → T11), tree-sitter expansion (T15 → T16), and incremental indexing (T18). T17 is the only addition to the critical path; everything else is parallel work. + +## Configuration + +| Variable | Description | Default | +|---|---|---| +| FALKORDB_HOST | FalkorDB hostname | localhost | +| FALKORDB_PORT | FalkorDB port | 6379 | +| MODEL_NAME | LLM for ask tool (LiteLLM format) | openai/gpt-4o-mini | +| LLM_API_KEY | API key for ask tool (optional) | — | +| CODE_GRAPH_AUTO_INDEX | Auto-index on first tool call | false | +| CODE_GRAPH_IGNORE | Dirs to ignore | node_modules,.git,__pycache__ | +| MCP_TRANSPORT | stdio or http | stdio | +| MCP_PORT | HTTP transport port | 3000 | + +Quick start (Claude Code): +```bash +claude mcp add-json "code-graph" '{"command":"cgraph-mcp","env":{"FALKORDB_HOST":"localhost","LLM_API_KEY":"sk-..."}}' +``` + +## Roadmap + +- **Phase 1:** MCP server with 8 tools. **11 languages** (Python/JS/Kotlin/Go/Rust/TypeScript/Ruby/C/C++ via tree-sitter; Java/C# via multilspy). Stdio transport. Auto-init. Agent guidance. **Per-branch graph identity. Default-on incremental indexing.** **18 vertical issues** (T1–T18). +- **Phase 1.5:** HTTP/SSE transport. Real-LLM nightly E2E with secret-managed API key. Prompt iteration on `ask` tool. +- **Phase 2:** Cross-branch query tools ("what changed between branches"). Per-file second-pass LSP optimization in incremental indexing. Rename detection in incremental flow. tree-sitter language coverage beyond the 11 (toward the 60+ baseline). Benchmarks. +- **Phase 3:** Dedicated TS/Go analyzers (replacing tree-sitter for those two with deeper LSP integration). Copilot extension. FalkorDB Cloud integration. Branch garbage collection / TTL. +- **Future:** Merge with Graphiti (memory) and mcpserver (raw graph) into unified `@falkordb/code-intelligence`. + +## Design Doc + +The full design document (v4) is in `code-graph-mcp-v4.docx` and covers: competitive landscape, architecture, tool catalog, data model, parsing strategy, GraphRAG integration, agent integration patterns, current state assessment, execution roadmap, success metrics, risks, and configuration reference. diff --git a/docs/code-graph-mcp-v4.docx b/docs/code-graph-mcp-v4.docx new file mode 100644 index 00000000..5b1064b6 Binary files /dev/null and b/docs/code-graph-mcp-v4.docx differ diff --git a/pyproject.toml b/pyproject.toml index 121c5b9f..95eb07d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,10 +23,12 @@ dependencies = [ "javatools>=1.6.0,<2.0.0", "pygit2>=1.17.0,<2.0.0", "typer>=0.24.0,<1.0.0", + "mcp>=1.0.0,<2.0.0", ] [project.scripts] cgraph = "api.cli:app" +cgraph-mcp = "api.mcp.server:main" [project.optional-dependencies] test = [ diff --git a/tests/mcp/__init__.py b/tests/mcp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/mcp/test_scaffold.py b/tests/mcp/test_scaffold.py new file mode 100644 index 00000000..72bb8d05 --- /dev/null +++ b/tests/mcp/test_scaffold.py @@ -0,0 +1,59 @@ +"""Scaffold smoke tests for the cgraph-mcp server (T1). + +These tests prove the bare module is wired correctly: + +1. The FastMCP ``app`` instance is importable. +2. The ``cgraph-mcp`` console script spawns a working stdio MCP server. +3. A client can complete the MCP handshake and ``list_tools`` returns 0 + tools (no tools are registered yet — they land in T4-T8, T11). + +When tool tickets land they should ADD tests, not modify these — these +guard the scaffold itself. +""" + +from __future__ import annotations + +import shutil + +import anyio +import pytest +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + +STDIO_TIMEOUT = 30 # seconds — prevents CI from hanging if the server fails to start + + +def test_app_is_importable() -> None: + """The FastMCP instance can be imported and is named ``code-graph``.""" + from api.mcp.server import app + + assert app is not None + assert app.name == "code-graph" + + +def test_main_entry_point_exists() -> None: + """``main()`` is exposed for the console script.""" + from api.mcp import server + + assert callable(server.main) + + +@pytest.mark.anyio +async def test_stdio_server_lists_zero_tools() -> None: + """Spawn ``cgraph-mcp`` over stdio and verify the protocol handshake. + + The scaffold registers no tools, so ``list_tools`` must return an + empty list. Tool tickets (T4-T8, T11) extend this expectation. + """ + cgraph_mcp = shutil.which("cgraph-mcp") + assert cgraph_mcp is not None, ( + "cgraph-mcp not on PATH; run `uv pip install -e .` first" + ) + + params = StdioServerParameters(command=cgraph_mcp, args=[]) + with anyio.fail_after(STDIO_TIMEOUT): + async with stdio_client(params) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + result = await session.list_tools() + assert result.tools == [] diff --git a/uv.lock b/uv.lock index 42d272fd..2f57f302 100644 --- a/uv.lock +++ b/uv.lock @@ -256,6 +256,45 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, +] + [[package]] name = "ct3" version = "3.4.0.post5" @@ -317,6 +356,7 @@ dependencies = [ { name = "fastapi" }, { name = "graphrag-sdk" }, { name = "javatools" }, + { name = "mcp" }, { name = "pygit2" }, { name = "python-dotenv" }, { name = "tree-sitter" }, @@ -346,6 +386,7 @@ requires-dist = [ { name = "graphrag-sdk", specifier = ">=0.8.1,<0.9.0" }, { name = "httpx", marker = "extra == 'test'", specifier = ">=0.28.0,<1.0.0" }, { name = "javatools", specifier = ">=1.6.0,<2.0.0" }, + { name = "mcp", specifier = ">=1.0.0,<2.0.0" }, { name = "pygit2", specifier = ">=1.17.0,<2.0.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0.2,<10.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1,<2.0.0" }, @@ -614,6 +655,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + [[package]] name = "huggingface-hub" version = "1.6.0" @@ -875,6 +925,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, ] +[[package]] +name = "mcp" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/eb/c0cfc62075dc6e1ec1c64d352ae09ac051d9334311ed226f1f425312848a/mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83", size = 607509, upload-time = "2026-04-02T14:48:08.88Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/46/f6b4ad632c67ef35209a66127e4bddc95759649dd595f71f13fba11bdf9a/mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741", size = 215967, upload-time = "2026-04-02T14:48:07.24Z" }, +] + [[package]] name = "mdurl" version = "0.1.2" @@ -1136,6 +1211,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + [[package]] name = "pygit2" version = "1.19.1" @@ -1189,6 +1278,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -1244,6 +1347,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1510,6 +1635,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/8c/f9290339ef6d79badbc010f067cd769d6601ec11a57d78569c683fb4dd87/sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1", size = 32427, upload-time = "2026-03-29T09:00:23.307Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/7f/3de5402f39890ac5660b86bcf5c03f9d855dad5c4ed764866d7b592b46fd/sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1", size = 14330, upload-time = "2026-03-29T09:00:21.846Z" }, +] + [[package]] name = "starlette" version = "0.52.1"