attocode-code-intel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
attocode_intel/server.py
ADDED
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
"""MCP server exposing Attocode's code intelligence capabilities.
|
|
2
|
+
|
|
3
|
+
Tool families (full list registered at runtime; query via your MCP
|
|
4
|
+
client's tool-listing endpoint):
|
|
5
|
+
|
|
6
|
+
- **Navigation & repo map**: bootstrap, relevant_context, repo_map,
|
|
7
|
+
symbols, search_symbols, file_analysis, project_summary,
|
|
8
|
+
explore_codebase, find_related, community_detection
|
|
9
|
+
- **Cross-refs & impact**: dependencies, impact_analysis,
|
|
10
|
+
cross_references, dependency_graph, graph_query, graph_dsl,
|
|
11
|
+
call_graph, explain_impact
|
|
12
|
+
- **LSP**: lsp_definition, lsp_references, lsp_hover, lsp_diagnostics
|
|
13
|
+
- **Search**: semantic_search, semantic_search_status, fast_search,
|
|
14
|
+
fuzzy_search, frecency_search, regex_search, cross_mode_search
|
|
15
|
+
- **Rules engine**: analyze, list_rules, list_packs, install_pack,
|
|
16
|
+
install_community_pack, register_rule, test_rules, ci_scan,
|
|
17
|
+
rule_stats, rule_feedback, rule_hygiene, synthesize_rule,
|
|
18
|
+
evolve_rules, import_rules
|
|
19
|
+
- **Quality / hygiene**: hotspots, conventions, dead_code, distill,
|
|
20
|
+
security_scan, suggest_tests, review_change, readiness_report
|
|
21
|
+
- **History**: code_evolution, recent_changes, churn_hotspots,
|
|
22
|
+
change_coupling
|
|
23
|
+
- **Memory & learning**: recall, record_learning, learning_feedback,
|
|
24
|
+
list_learnings, record_adr, list_adrs, get_adr, update_adr_status
|
|
25
|
+
|
|
26
|
+
Language packs shipped: **go, python, typescript, rust, java, cpp,
|
|
27
|
+
csharp, php, ruby, kotlin** (10 packs, ~91 rules — Tier-1 regex plus
|
|
28
|
+
Tier-2 ast-grep structural patterns where ast-grep is on PATH).
|
|
29
|
+
|
|
30
|
+
Usage::
|
|
31
|
+
|
|
32
|
+
attocode-code-intel --project /path/to/repo
|
|
33
|
+
|
|
34
|
+
Remote mode (proxy through a remote code-intel HTTP server)::
|
|
35
|
+
|
|
36
|
+
attocode-code-intel --project . --remote https://code.example.com --remote-token <jwt> --remote-repo <uuid>
|
|
37
|
+
|
|
38
|
+
Remote connection can also be auto-loaded from ``.attocode/config.toml``
|
|
39
|
+
(created by ``attocode code-intel connect``).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import logging
|
|
45
|
+
import os
|
|
46
|
+
import sys
|
|
47
|
+
import threading
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
import attocode_intel._shared as _shared # noqa: F401
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Shared deps (mcp instance, lazy singletons, getters) live in _shared.py
|
|
54
|
+
# to break the circular import: server.py → tool modules → server.py.
|
|
55
|
+
# Re-exported here for backward compatibility.
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
from attocode_intel._shared import ( # noqa: F401
|
|
58
|
+
_get_ast_service,
|
|
59
|
+
_get_code_analyzer,
|
|
60
|
+
_get_context_mgr,
|
|
61
|
+
_get_explorer,
|
|
62
|
+
_get_project_dir,
|
|
63
|
+
_walk_up,
|
|
64
|
+
clear_remote_service,
|
|
65
|
+
configure_remote_service,
|
|
66
|
+
enable_remote_if_configured,
|
|
67
|
+
mcp,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
logger = logging.getLogger(__name__)
|
|
71
|
+
|
|
72
|
+
# Backward-compat: tests and internal code set server._ast_service = mock.
|
|
73
|
+
# The getters (in _shared) read _shared._ast_service, so we must ensure
|
|
74
|
+
# that setting server._ast_service also sets _shared._ast_service.
|
|
75
|
+
# We do this by NOT declaring local variables here and using __getattr__
|
|
76
|
+
# to proxy reads, plus __setattr__ to proxy writes via setattr().
|
|
77
|
+
_SINGLETON_VARS = frozenset({
|
|
78
|
+
"_ast_service", "_context_mgr", "_code_analyzer",
|
|
79
|
+
"_semantic_search", "_memory_store", "_explorer",
|
|
80
|
+
"_service", "_remote_service",
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
def __getattr__(name: str):
|
|
84
|
+
if name in _SINGLETON_VARS:
|
|
85
|
+
return getattr(_shared, name, None)
|
|
86
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# MCP Resource: Agent Guidelines
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
_GUIDELINES_PATH = Path(__file__).parent / "GUIDELINES.md"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@mcp.resource("attocode://guidelines")
|
|
96
|
+
def guidelines_resource() -> str:
|
|
97
|
+
"""Agent guidelines for using code intelligence tools effectively."""
|
|
98
|
+
try:
|
|
99
|
+
return _GUIDELINES_PATH.read_text(encoding="utf-8")
|
|
100
|
+
except OSError:
|
|
101
|
+
return "Guidelines file not found."
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# MCP Resources: Project files and symbol lookup
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@mcp.resource("attocode://project/{path}")
|
|
110
|
+
def project_file_resource(path: str) -> str:
|
|
111
|
+
"""File content with line numbers. Path-traversal protected.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
path: Relative file path within the project.
|
|
115
|
+
"""
|
|
116
|
+
project_dir = _get_project_dir()
|
|
117
|
+
root = Path(os.path.realpath(project_dir))
|
|
118
|
+
full = Path(os.path.realpath(os.path.join(project_dir, path)))
|
|
119
|
+
if not full.is_relative_to(root):
|
|
120
|
+
return "Error: path traversal not allowed."
|
|
121
|
+
if not full.is_file():
|
|
122
|
+
return f"Error: file not found: {path}"
|
|
123
|
+
try:
|
|
124
|
+
content = full.read_text(encoding="utf-8", errors="replace")
|
|
125
|
+
lines = content.split("\n")
|
|
126
|
+
numbered = [f"{i + 1:>5} | {line}" for i, line in enumerate(lines)]
|
|
127
|
+
return "\n".join(numbered)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
return f"Error reading file: {e}"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.resource("attocode://symbols/{name}")
|
|
133
|
+
def symbol_resource(name: str) -> str:
|
|
134
|
+
"""Symbol definition lookup with source snippets.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
name: Symbol name to look up.
|
|
138
|
+
"""
|
|
139
|
+
svc = _get_ast_service()
|
|
140
|
+
defs = svc.find_symbol(name)
|
|
141
|
+
if not defs:
|
|
142
|
+
return f"No definitions found for '{name}'."
|
|
143
|
+
|
|
144
|
+
project_dir = _get_project_dir()
|
|
145
|
+
root = Path(os.path.realpath(project_dir))
|
|
146
|
+
|
|
147
|
+
lines = [f"Definitions for '{name}':"]
|
|
148
|
+
for d in defs[:10]:
|
|
149
|
+
lines.append(f"\n {d.file_path}:{d.start_line}")
|
|
150
|
+
lines.append(f" Qualified: {d.qualified_name}")
|
|
151
|
+
# Try to read source snippet (path-traversal protected)
|
|
152
|
+
try:
|
|
153
|
+
full = Path(os.path.realpath(os.path.join(project_dir, d.file_path)))
|
|
154
|
+
if not full.is_relative_to(root):
|
|
155
|
+
continue
|
|
156
|
+
src_lines = full.read_text(encoding="utf-8", errors="replace").split("\n")
|
|
157
|
+
start = max(0, d.start_line - 1)
|
|
158
|
+
end = min(len(src_lines), d.end_line + 1)
|
|
159
|
+
for i in range(start, end):
|
|
160
|
+
lines.append(f" {i + 1:>5} | {src_lines[i]}")
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return "\n".join(lines)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@mcp.resource("attocode://learnings")
|
|
167
|
+
def learnings_resource() -> str:
|
|
168
|
+
"""All active project learnings. Read this for full project knowledge base."""
|
|
169
|
+
from attocode_intel.tools.learning_tools import _get_memory_store
|
|
170
|
+
|
|
171
|
+
store = _get_memory_store()
|
|
172
|
+
results = store.list_all(status="active")
|
|
173
|
+
if not results:
|
|
174
|
+
return "No project learnings recorded yet."
|
|
175
|
+
|
|
176
|
+
lines = ["# Project Learnings\n"]
|
|
177
|
+
by_type: dict[str, list[dict]] = {}
|
|
178
|
+
for r in results:
|
|
179
|
+
by_type.setdefault(r["type"], []).append(r)
|
|
180
|
+
|
|
181
|
+
for type_name, entries in sorted(by_type.items()):
|
|
182
|
+
lines.append(f"\n## {type_name.title()} ({len(entries)})\n")
|
|
183
|
+
for r in entries:
|
|
184
|
+
scope_tag = f" [{r['scope']}]" if r["scope"] else ""
|
|
185
|
+
conf = f"{r['confidence']:.0%}"
|
|
186
|
+
lines.append(f"- **{r['description']}**{scope_tag} (id: {r['id']}, confidence: {conf})")
|
|
187
|
+
if r["details"]:
|
|
188
|
+
lines.append(f" {r['details']}")
|
|
189
|
+
return "\n".join(lines)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Notification tool (explicit index update)
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@mcp.tool()
|
|
198
|
+
def notify_file_changed(files: list[str]) -> str:
|
|
199
|
+
"""Tell the server which files changed, so that the index and search read the new content.
|
|
200
|
+
|
|
201
|
+
A server that watches the project finds edits without this call.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
files: Changed file paths, relative or absolute.
|
|
205
|
+
"""
|
|
206
|
+
if not files:
|
|
207
|
+
return "No files specified."
|
|
208
|
+
|
|
209
|
+
project_dir = _get_project_dir()
|
|
210
|
+
svc = _get_ast_service()
|
|
211
|
+
updated = 0
|
|
212
|
+
|
|
213
|
+
for f in files:
|
|
214
|
+
try:
|
|
215
|
+
p = Path(f)
|
|
216
|
+
rel = os.path.relpath(str(p), project_dir) if p.is_absolute() else str(p)
|
|
217
|
+
# Guard against path traversal
|
|
218
|
+
rel = os.path.normpath(rel)
|
|
219
|
+
if rel.startswith(".."):
|
|
220
|
+
continue
|
|
221
|
+
svc.notify_file_changed(rel)
|
|
222
|
+
# Also invalidate semantic search embeddings
|
|
223
|
+
try:
|
|
224
|
+
smgr = _semantic_search
|
|
225
|
+
if smgr is None:
|
|
226
|
+
from attocode_intel.tools.search_tools import _get_semantic_search
|
|
227
|
+
smgr = _get_semantic_search()
|
|
228
|
+
abs_path = os.path.join(project_dir, rel)
|
|
229
|
+
smgr.invalidate_file(abs_path)
|
|
230
|
+
smgr.queue_reindex(abs_path)
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
# Also invalidate trigram index
|
|
234
|
+
try:
|
|
235
|
+
from attocode_intel.tools.search_tools import _get_trigram_index
|
|
236
|
+
tri_idx = _get_trigram_index()
|
|
237
|
+
if tri_idx is not None:
|
|
238
|
+
abs_path = os.path.join(project_dir, rel)
|
|
239
|
+
tri_idx.update_file(rel, b"")
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
updated += 1
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
logger.debug("notify_file_changed: error for %s: %s", f, exc)
|
|
245
|
+
|
|
246
|
+
# Also drain the notification queue (CLI-written)
|
|
247
|
+
queued = _process_notification_queue()
|
|
248
|
+
|
|
249
|
+
total = updated + queued
|
|
250
|
+
return f"Updated {total} file(s). AST index refreshed."
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
# Notification queue (CLI -> server communication)
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _get_queue_path() -> Path:
|
|
259
|
+
"""Return the path to the notification queue file."""
|
|
260
|
+
return Path(_get_project_dir()) / ".attocode" / "cache" / "file_changes"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
_queue_lock = threading.Lock()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _process_notification_queue() -> int:
|
|
267
|
+
"""Check the notification queue file and process pending changes.
|
|
268
|
+
|
|
269
|
+
Returns number of files processed.
|
|
270
|
+
"""
|
|
271
|
+
with _queue_lock:
|
|
272
|
+
return _process_notification_queue_locked()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _process_notification_queue_locked() -> int:
|
|
276
|
+
"""Inner implementation -- must be called under ``_queue_lock``."""
|
|
277
|
+
try:
|
|
278
|
+
queue_path = _get_queue_path()
|
|
279
|
+
except RuntimeError:
|
|
280
|
+
return 0
|
|
281
|
+
|
|
282
|
+
if not queue_path.exists():
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# Atomic read-and-truncate under exclusive lock to avoid TOCTOU
|
|
287
|
+
try:
|
|
288
|
+
import fcntl
|
|
289
|
+
with open(queue_path, "r+", encoding="utf-8") as fh:
|
|
290
|
+
fcntl.flock(fh, fcntl.LOCK_EX)
|
|
291
|
+
content = fh.read()
|
|
292
|
+
fh.seek(0)
|
|
293
|
+
fh.truncate()
|
|
294
|
+
except ImportError:
|
|
295
|
+
# Windows: fall back to non-locked read+truncate
|
|
296
|
+
content = queue_path.read_text(encoding="utf-8")
|
|
297
|
+
queue_path.write_text("", encoding="utf-8")
|
|
298
|
+
except OSError:
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
paths = [line.strip() for line in content.splitlines() if line.strip()]
|
|
302
|
+
if not paths:
|
|
303
|
+
return 0
|
|
304
|
+
|
|
305
|
+
project_dir = _get_project_dir()
|
|
306
|
+
svc = _get_ast_service()
|
|
307
|
+
count = 0
|
|
308
|
+
|
|
309
|
+
for rel in paths:
|
|
310
|
+
# Guard against path traversal (e.g. ../../etc/passwd)
|
|
311
|
+
norm = os.path.normpath(rel)
|
|
312
|
+
if norm.startswith(".."):
|
|
313
|
+
continue
|
|
314
|
+
try:
|
|
315
|
+
svc.notify_file_changed(norm)
|
|
316
|
+
try:
|
|
317
|
+
smgr = _semantic_search
|
|
318
|
+
if smgr is None:
|
|
319
|
+
from attocode_intel.tools.search_tools import _get_semantic_search
|
|
320
|
+
smgr = _get_semantic_search()
|
|
321
|
+
abs_path = os.path.join(project_dir, norm)
|
|
322
|
+
smgr.invalidate_file(abs_path)
|
|
323
|
+
smgr.queue_reindex(abs_path)
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
try:
|
|
327
|
+
from attocode_intel.tools.search_tools import _get_trigram_index
|
|
328
|
+
tri_idx = _get_trigram_index()
|
|
329
|
+
if tri_idx is not None:
|
|
330
|
+
tri_idx.update_file(norm, b"")
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
count += 1
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
|
|
337
|
+
if count:
|
|
338
|
+
logger.debug("Processed %d queued file notification(s)", count)
|
|
339
|
+
return count
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
_queue_thread: threading.Thread | None = None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _start_queue_poller(project_dir: str) -> None:
|
|
346
|
+
"""Start a background thread that polls the notification queue every 2s."""
|
|
347
|
+
global _queue_thread
|
|
348
|
+
|
|
349
|
+
if _queue_thread is not None:
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
def _poll_loop() -> None:
|
|
353
|
+
while not _watcher_stop.is_set():
|
|
354
|
+
try:
|
|
355
|
+
_process_notification_queue()
|
|
356
|
+
except Exception:
|
|
357
|
+
pass
|
|
358
|
+
_watcher_stop.wait(2.0)
|
|
359
|
+
|
|
360
|
+
_queue_thread = threading.Thread(
|
|
361
|
+
target=_poll_loop, daemon=True, name="code-intel-queue-poller"
|
|
362
|
+
)
|
|
363
|
+
_queue_thread.start()
|
|
364
|
+
logger.debug("Notification queue poller started")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
# File watcher (background thread)
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
|
|
371
|
+
_CODE_EXTENSIONS = {
|
|
372
|
+
".py", ".js", ".jsx", ".ts", ".tsx", ".go", ".rs", ".java",
|
|
373
|
+
".rb", ".c", ".cpp", ".h", ".hpp", ".cs", ".swift", ".kt",
|
|
374
|
+
".scala", ".sc", ".ex", ".exs", ".zig", ".hs", ".tf", ".tfvars",
|
|
375
|
+
".hcl", ".lua", ".sh", ".bash", ".php",
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
_watcher_thread: threading.Thread | None = None
|
|
379
|
+
_watcher_stop = threading.Event()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _start_file_watcher(project_dir: str, *, debounce_ms: int = 500) -> None:
|
|
383
|
+
"""Start a background file watcher that updates the AST index on changes.
|
|
384
|
+
|
|
385
|
+
Uses watchfiles (Rust-backed) if available, otherwise skips silently.
|
|
386
|
+
Calls ASTService.notify_file_changed() and queues background reindex for
|
|
387
|
+
modified code files. Second call is a no-op (idempotent).
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
project_dir: Absolute path to the project root.
|
|
391
|
+
debounce_ms: Debounce interval in milliseconds (default 500ms).
|
|
392
|
+
"""
|
|
393
|
+
global _watcher_thread
|
|
394
|
+
|
|
395
|
+
if _watcher_thread is not None:
|
|
396
|
+
return # Already running
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
from watchfiles import Change, watch
|
|
400
|
+
except ImportError:
|
|
401
|
+
logger.debug("watchfiles not installed — file watcher disabled")
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
def _watcher_loop() -> None:
|
|
405
|
+
try:
|
|
406
|
+
for changes in watch(
|
|
407
|
+
project_dir,
|
|
408
|
+
stop_event=_watcher_stop,
|
|
409
|
+
recursive=True,
|
|
410
|
+
debounce=debounce_ms,
|
|
411
|
+
# Ignore hidden dirs, node_modules, __pycache__, .git
|
|
412
|
+
watch_filter=lambda _, path: (
|
|
413
|
+
not any(
|
|
414
|
+
part.startswith(".") or part in ("node_modules", "__pycache__", ".git")
|
|
415
|
+
for part in Path(path).parts
|
|
416
|
+
)
|
|
417
|
+
and Path(path).suffix.lower() in _CODE_EXTENSIONS
|
|
418
|
+
),
|
|
419
|
+
):
|
|
420
|
+
if _watcher_stop.is_set():
|
|
421
|
+
break
|
|
422
|
+
|
|
423
|
+
svc = _get_ast_service()
|
|
424
|
+
for change_type, path_str in changes:
|
|
425
|
+
if change_type in (Change.modified, Change.added, Change.deleted):
|
|
426
|
+
try:
|
|
427
|
+
rel = os.path.relpath(path_str, project_dir)
|
|
428
|
+
svc.notify_file_changed(rel)
|
|
429
|
+
# Also invalidate semantic search embeddings
|
|
430
|
+
try:
|
|
431
|
+
from attocode_intel.tools.search_tools import (
|
|
432
|
+
_get_semantic_search,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
smgr = _get_semantic_search()
|
|
436
|
+
smgr.invalidate_file(path_str)
|
|
437
|
+
smgr.queue_reindex(path_str)
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
try:
|
|
441
|
+
from attocode_intel.tools.search_tools import (
|
|
442
|
+
_get_trigram_index,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
tri_idx = _get_trigram_index()
|
|
446
|
+
if tri_idx is not None:
|
|
447
|
+
tri_idx.update_file(rel, b"")
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
logger.debug("File watcher: updated %s", rel)
|
|
451
|
+
except Exception:
|
|
452
|
+
pass # Best-effort -- don't crash the watcher
|
|
453
|
+
except Exception:
|
|
454
|
+
logger.debug("File watcher stopped", exc_info=True)
|
|
455
|
+
|
|
456
|
+
_watcher_thread = threading.Thread(
|
|
457
|
+
target=_watcher_loop, daemon=True, name="code-intel-watcher"
|
|
458
|
+
)
|
|
459
|
+
_watcher_thread.start()
|
|
460
|
+
logger.info("File watcher started for %s (debounce=%dms)", project_dir, debounce_ms)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _stop_file_watcher() -> None:
|
|
464
|
+
"""Stop the background file watcher and queue poller."""
|
|
465
|
+
global _watcher_thread, _queue_thread
|
|
466
|
+
_watcher_stop.set()
|
|
467
|
+
for t in (_watcher_thread, _queue_thread):
|
|
468
|
+
if t is not None:
|
|
469
|
+
t.join(timeout=2.0)
|
|
470
|
+
_watcher_thread = None
|
|
471
|
+
_queue_thread = None
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
# Tool instrumentation — wraps MCP tool functions with metrics recording
|
|
476
|
+
# ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _instrument_tool(tool_fn, tool_name: str):
|
|
480
|
+
"""Wrap an MCP tool function to record call metrics.
|
|
481
|
+
|
|
482
|
+
Records duration and success/failure for each invocation via the
|
|
483
|
+
module-level ``metrics_collector`` singleton.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
tool_fn: The original tool function (sync).
|
|
487
|
+
tool_name: Human-readable tool name for metric labels.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Wrapped function that records metrics then delegates to *tool_fn*.
|
|
491
|
+
"""
|
|
492
|
+
import functools
|
|
493
|
+
import time as _time
|
|
494
|
+
|
|
495
|
+
@functools.wraps(tool_fn)
|
|
496
|
+
def _wrapper(*args, **kwargs):
|
|
497
|
+
from attocode_intel.api.middleware import metrics_collector
|
|
498
|
+
|
|
499
|
+
start = _time.monotonic()
|
|
500
|
+
success = True
|
|
501
|
+
try:
|
|
502
|
+
result = tool_fn(*args, **kwargs)
|
|
503
|
+
return result
|
|
504
|
+
except Exception:
|
|
505
|
+
success = False
|
|
506
|
+
raise
|
|
507
|
+
finally:
|
|
508
|
+
duration_ms = (_time.monotonic() - start) * 1000
|
|
509
|
+
metrics_collector.record_tool_call(tool_name, duration_ms, success)
|
|
510
|
+
|
|
511
|
+
return _wrapper
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _instrument_all_tools() -> None:
|
|
515
|
+
"""Wrap all registered MCP tools with metrics instrumentation.
|
|
516
|
+
|
|
517
|
+
Must be called after all tool modules have been imported (so that
|
|
518
|
+
``@mcp.tool()`` decorators have fired). Patches the internal tool
|
|
519
|
+
registry in-place.
|
|
520
|
+
"""
|
|
521
|
+
try:
|
|
522
|
+
# FastMCP stores tools in mcp._tool_manager._tools (dict[str, Tool])
|
|
523
|
+
tool_manager = getattr(mcp, "_tool_manager", None)
|
|
524
|
+
if tool_manager is None:
|
|
525
|
+
return
|
|
526
|
+
tools_dict = getattr(tool_manager, "_tools", None)
|
|
527
|
+
if tools_dict is None:
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
for name, tool_obj in tools_dict.items():
|
|
531
|
+
original_fn = getattr(tool_obj, "fn", None)
|
|
532
|
+
if original_fn is None:
|
|
533
|
+
continue
|
|
534
|
+
tool_obj.fn = _instrument_tool(original_fn, name)
|
|
535
|
+
logger.info("Instrumented %d MCP tools with metrics recording", len(tools_dict))
|
|
536
|
+
except Exception:
|
|
537
|
+
logger.debug("Failed to instrument MCP tools", exc_info=True)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# ---------------------------------------------------------------------------
|
|
541
|
+
# Register all tool modules (decorators fire on import)
|
|
542
|
+
# ---------------------------------------------------------------------------
|
|
543
|
+
|
|
544
|
+
import attocode_intel.tools.adr_tools as _adr_tools # noqa: E402, F401
|
|
545
|
+
import attocode_intel.tools.analysis_tools as _analysis_tools # noqa: E402, F401
|
|
546
|
+
import attocode_intel.tools.cross_mode_tools as _cross_mode_tools # noqa: E402, F401
|
|
547
|
+
import attocode_intel.tools.dead_code_tools as _dead_code_tools # noqa: E402, F401
|
|
548
|
+
import attocode_intel.tools.distill_tools as _distill_tools # noqa: E402, F401
|
|
549
|
+
import attocode_intel.tools.frecency_tools as _frecency_tools # noqa: E402, F401
|
|
550
|
+
import attocode_intel.tools.fuzzy_tools as _fuzzy_tools # noqa: E402, F401
|
|
551
|
+
import attocode_intel.tools.history_tools as _history_tools # noqa: E402, F401
|
|
552
|
+
import attocode_intel.tools.learning_tools as _learning_tools # noqa: E402, F401
|
|
553
|
+
import attocode_intel.tools.lsp_tools as _lsp_tools # noqa: E402, F401
|
|
554
|
+
import attocode_intel.tools.navigation_tools as _navigation_tools # noqa: E402, F401
|
|
555
|
+
import attocode_intel.tools.query_constraints_tools as _query_constraints_tools # noqa: E402, F401
|
|
556
|
+
import attocode_intel.tools.query_history_tools as _query_history_tools # noqa: E402, F401
|
|
557
|
+
import attocode_intel.tools.readiness_tools as _readiness_tools # noqa: E402, F401
|
|
558
|
+
import attocode_intel.tools.composite_tools as _composite_tools # noqa: E402, F401
|
|
559
|
+
import attocode_intel.tools.search_tools as _search_tools # noqa: E402, F401
|
|
560
|
+
# pin_tools must come first because maintenance_tools imports
|
|
561
|
+
# _stamp_pin / _STORE_DEFS from it.
|
|
562
|
+
import attocode_intel.tools.pin_tools as _pin_tools # noqa: E402, F401
|
|
563
|
+
import attocode_intel.tools.maintenance_tools as _maintenance_tools # noqa: E402, F401
|
|
564
|
+
import attocode_intel.tools.snapshot_tools as _snapshot_tools # noqa: E402, F401
|
|
565
|
+
import attocode_intel.tools.overlay_tools as _overlay_tools # noqa: E402, F401
|
|
566
|
+
# Rule-based analysis engine (PerfInsights-inspired)
|
|
567
|
+
import attocode_intel.tools.rule_tools as _rule_tools # noqa: E402, F401
|
|
568
|
+
from attocode_intel.helpers import ( # noqa: E402, F401
|
|
569
|
+
_compute_file_metrics,
|
|
570
|
+
_percentile_ranks,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
# Backward-compatible re-exports via attribute alias (avoids circular import)
|
|
574
|
+
file_analysis = _analysis_tools.file_analysis # noqa: E402
|
|
575
|
+
impact_analysis = _analysis_tools.impact_analysis # noqa: E402
|
|
576
|
+
dependency_graph = _analysis_tools.dependency_graph # noqa: E402
|
|
577
|
+
hotspots = _analysis_tools.hotspots # noqa: E402
|
|
578
|
+
cross_references = _analysis_tools.cross_references # noqa: E402
|
|
579
|
+
dependencies = _analysis_tools.dependencies # noqa: E402
|
|
580
|
+
explore_codebase = _navigation_tools.explore_codebase # noqa: E402
|
|
581
|
+
find_related = _analysis_tools.find_related # noqa: E402
|
|
582
|
+
community_detection = _analysis_tools.community_detection # noqa: E402
|
|
583
|
+
graph_dsl = _analysis_tools.graph_dsl # noqa: E402
|
|
584
|
+
|
|
585
|
+
semantic_search = _search_tools.semantic_search # noqa: E402
|
|
586
|
+
semantic_search_status = _search_tools.semantic_search_status # noqa: E402
|
|
587
|
+
security_scan = _search_tools.security_scan # noqa: E402
|
|
588
|
+
fast_search = _search_tools.fast_search # noqa: E402
|
|
589
|
+
|
|
590
|
+
recall = _learning_tools.recall # noqa: E402
|
|
591
|
+
record_learning = _learning_tools.record_learning # noqa: E402
|
|
592
|
+
learning_feedback = _learning_tools.learning_feedback # noqa: E402
|
|
593
|
+
list_learnings = _learning_tools.list_learnings # noqa: E402
|
|
594
|
+
|
|
595
|
+
record_adr = _adr_tools.record_adr # noqa: E402
|
|
596
|
+
list_adrs = _adr_tools.list_adrs # noqa: E402
|
|
597
|
+
get_adr = _adr_tools.get_adr # noqa: E402
|
|
598
|
+
update_adr_status = _adr_tools.update_adr_status # noqa: E402
|
|
599
|
+
|
|
600
|
+
dead_code = _dead_code_tools.dead_code # noqa: E402
|
|
601
|
+
distill = _distill_tools.distill # noqa: E402
|
|
602
|
+
readiness_report = _readiness_tools.readiness_report # noqa: E402
|
|
603
|
+
|
|
604
|
+
code_evolution = _history_tools.code_evolution # noqa: E402
|
|
605
|
+
recent_changes = _history_tools.recent_changes # noqa: E402
|
|
606
|
+
|
|
607
|
+
review_change = _composite_tools.review_change # noqa: E402
|
|
608
|
+
explain_impact = _composite_tools.explain_impact # noqa: E402
|
|
609
|
+
suggest_tests = _composite_tools.suggest_tests # noqa: E402
|
|
610
|
+
architecture_drift = _composite_tools.architecture_drift # noqa: E402
|
|
611
|
+
|
|
612
|
+
bootstrap = _navigation_tools.bootstrap # noqa: E402
|
|
613
|
+
conventions = _navigation_tools.conventions # noqa: E402
|
|
614
|
+
project_summary = _navigation_tools.project_summary # noqa: E402
|
|
615
|
+
relevant_context = _navigation_tools.relevant_context # noqa: E402
|
|
616
|
+
repo_map = _navigation_tools.repo_map # noqa: E402
|
|
617
|
+
search_symbols = _navigation_tools.search_symbols # noqa: E402
|
|
618
|
+
symbols = _navigation_tools.symbols # noqa: E402
|
|
619
|
+
|
|
620
|
+
# Instrument all registered tools with metrics recording
|
|
621
|
+
_instrument_all_tools()
|
|
622
|
+
|
|
623
|
+
# ---------------------------------------------------------------------------
|
|
624
|
+
# Entry point
|
|
625
|
+
# ---------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
# Subcommands that should be dispatched to the CLI handler instead of
|
|
628
|
+
# starting the MCP server.
|
|
629
|
+
_CLI_SUBCOMMANDS = {
|
|
630
|
+
"install", "uninstall", "serve", "status", "probe-install", "notify",
|
|
631
|
+
"connect", "test-connection", "watch", "help", "--help", "-h",
|
|
632
|
+
"query", "symbols", "impact", "hotspots", "deps", "dead-code",
|
|
633
|
+
"gc", "verify", "reindex", "bundle",
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def main() -> None:
|
|
638
|
+
"""CLI entry point for the MCP server.
|
|
639
|
+
|
|
640
|
+
If the first positional argument is a known subcommand (install, uninstall,
|
|
641
|
+
notify, status, serve, help), delegates to ``cli.dispatch_code_intel``.
|
|
642
|
+
Otherwise starts the MCP server on stdio.
|
|
643
|
+
"""
|
|
644
|
+
args = sys.argv[1:]
|
|
645
|
+
|
|
646
|
+
# Detect subcommands -- delegate to CLI dispatcher
|
|
647
|
+
if args and args[0] in _CLI_SUBCOMMANDS:
|
|
648
|
+
from attocode_intel.cli import dispatch_code_intel
|
|
649
|
+
|
|
650
|
+
dispatch_code_intel(args)
|
|
651
|
+
return
|
|
652
|
+
|
|
653
|
+
# No subcommand -- start MCP server
|
|
654
|
+
from attocode_intel.project_dir import find_project_root
|
|
655
|
+
|
|
656
|
+
project_dir = find_project_root(os.path.abspath("."))
|
|
657
|
+
|
|
658
|
+
transport = "stdio"
|
|
659
|
+
host = "127.0.0.1"
|
|
660
|
+
port = 8080
|
|
661
|
+
remote_url = ""
|
|
662
|
+
remote_token = ""
|
|
663
|
+
remote_repo_id = ""
|
|
664
|
+
local_only = os.environ.get("ATTOCODE_LOCAL_ONLY") == "1"
|
|
665
|
+
for i, arg in enumerate(args):
|
|
666
|
+
if arg == "--project" and i + 1 < len(args):
|
|
667
|
+
project_dir = args[i + 1]
|
|
668
|
+
elif arg.startswith("--project="):
|
|
669
|
+
project_dir = arg.split("=", 1)[1]
|
|
670
|
+
elif arg == "--transport" and i + 1 < len(args):
|
|
671
|
+
transport = args[i + 1]
|
|
672
|
+
elif arg.startswith("--transport="):
|
|
673
|
+
transport = arg.split("=", 1)[1]
|
|
674
|
+
elif arg == "--host" and i + 1 < len(args):
|
|
675
|
+
host = args[i + 1]
|
|
676
|
+
elif arg.startswith("--host="):
|
|
677
|
+
host = arg.split("=", 1)[1]
|
|
678
|
+
elif arg == "--port" and i + 1 < len(args):
|
|
679
|
+
port = int(args[i + 1])
|
|
680
|
+
elif arg.startswith("--port="):
|
|
681
|
+
port = int(arg.split("=", 1)[1])
|
|
682
|
+
elif arg == "--remote" and i + 1 < len(args):
|
|
683
|
+
remote_url = args[i + 1]
|
|
684
|
+
elif arg.startswith("--remote="):
|
|
685
|
+
remote_url = arg.split("=", 1)[1]
|
|
686
|
+
elif arg == "--remote-token" and i + 1 < len(args):
|
|
687
|
+
remote_token = args[i + 1]
|
|
688
|
+
elif arg.startswith("--remote-token="):
|
|
689
|
+
remote_token = arg.split("=", 1)[1]
|
|
690
|
+
elif arg == "--remote-repo" and i + 1 < len(args):
|
|
691
|
+
remote_repo_id = args[i + 1]
|
|
692
|
+
elif arg.startswith("--remote-repo="):
|
|
693
|
+
remote_repo_id = arg.split("=", 1)[1]
|
|
694
|
+
elif arg == "--local-only":
|
|
695
|
+
local_only = True
|
|
696
|
+
|
|
697
|
+
project_dir = os.path.abspath(project_dir)
|
|
698
|
+
os.environ["ATTOCODE_PROJECT_DIR"] = project_dir
|
|
699
|
+
if local_only:
|
|
700
|
+
os.environ["ATTOCODE_LOCAL_ONLY"] = "1"
|
|
701
|
+
|
|
702
|
+
# Enable remote mode from CLI flags / config.toml / env (shared with the
|
|
703
|
+
# CLI serve path); gracefully falls back to local on a dead/expired remote.
|
|
704
|
+
enable_remote_if_configured(
|
|
705
|
+
project_dir,
|
|
706
|
+
local_only=local_only,
|
|
707
|
+
remote_url=remote_url,
|
|
708
|
+
remote_token=remote_token,
|
|
709
|
+
remote_repo_id=remote_repo_id,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
# Start file watcher and notification queue poller in background
|
|
713
|
+
_start_file_watcher(project_dir)
|
|
714
|
+
_start_queue_poller(project_dir)
|
|
715
|
+
|
|
716
|
+
logger.info("Starting attocode-code-intel for %s (transport=%s)", project_dir, transport)
|
|
717
|
+
try:
|
|
718
|
+
if transport == "http":
|
|
719
|
+
from attocode_intel.cli import _serve_http
|
|
720
|
+
|
|
721
|
+
_serve_http(project_dir, host=host, port=port, debug=False)
|
|
722
|
+
elif transport == "sse":
|
|
723
|
+
mcp.run(transport="sse", host=host, port=port)
|
|
724
|
+
else:
|
|
725
|
+
from attocode_intel.entrypoint import exit_when_client_exits
|
|
726
|
+
|
|
727
|
+
exit_when_client_exits()
|
|
728
|
+
mcp.run(transport="stdio")
|
|
729
|
+
finally:
|
|
730
|
+
_stop_file_watcher()
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
if __name__ == "__main__":
|
|
734
|
+
main()
|