AGENTS.md
January 9, 2026 · View on GitHub
Guidelines for AI coding assistants working with this repository.
For domain-specific guidance, see subdirectory AGENTS.md files:
tests/AGENTS.md- Testing conventions and workflowsplugins/AGENTS.md- Plugin framework and developmentcharts/AGENTS.md- Helm chart operationsdocs/AGENTS.md- Documentation authoringmcp-servers/AGENTS.md- MCP server implementationcrates/mcp_runtime/DEVELOPING.md- Rust MCP runtime development workflows, command matrix, and validation
Note: The llms/ directory contains guidance for LLMs using ContextForge solution (end-user runtime guidance), not for code agents working on this codebase.
Project Overview
ContextForge is an open source registry and proxy that federates MCP, A2A, and REST/gRPC APIs with centralized governance, discovery, and observability. It federates tools, agents, and APIs, optimizes agent and tool calling, and supports plugins, auth/RBAC, rate-limiting, virtual servers, multi-transport protocols, and an optional Admin UI.
Project Structure
mcpgateway/ # Core FastAPI application
├── main.py # Application entry point
├── config.py # Environment configuration
├── db.py # SQLAlchemy ORM models and session management
├── schemas.py # Pydantic validation schemas
├── services/ # Business logic layer (50+ services)
├── routers/ # HTTP endpoint definitions (19 routers)
├── middleware/ # Cross-cutting concerns (16 middleware)
├── transports/ # Protocol implementations (SSE, WebSocket, stdio, streamable HTTP)
├── plugins/ # Plugin integration (uses cpex external package)
└── alembic/ # Database migrations
tests/ # Test suite (see tests/AGENTS.md)
plugins/ # Plugin implementations (see plugins/AGENTS.md)
plugins_rust/ # Rust plugin implementations for performance-sensitive paths
charts/ # Helm charts (see charts/AGENTS.md)
docs/ # Architecture and usage documentation (see docs/AGENTS.md)
a2a-agents/ # A2A agent implementations (used for testing/examples)
mcp-servers/ # MCP server templates (see mcp-servers/AGENTS.md)
crates/ # Direct Rust crate folders (runtime and wrapper)
llms/ # End-user LLM guidance (not for code agents)
Essential Commands
Setup
cp .env.example .env && make install-dev check-env # Complete setup
make venv # Create virtual environment with uv
make install-dev # Install with dev dependencies (includes build-ui)
make check-env # Verify .env against .env.example
make build-ui # Rebuild Admin UI JS bundle (requires npm)
Development
make dev # Dev server on :8000 with autoreload
make serve # Production gunicorn on :4444
make serve-ssl # HTTPS on :4444 (creates certs if needed)
Code Quality
# After writing code
make pre-commit
# Before committing, use ty, mypy and pyrefly to check just the new files, then run:
make ruff bandit interrogate pylint verify
# Before committing Rust changes (tools_rust/):
# Run fmt-check, clippy -D warnings, and cargo test for Rust crates
cd tools_rust/mcp_runtime && cargo fmt --check && cargo clippy -- -D warnings && cargo test
PR Review Workflow
Standard prompt: "Rebase against main, then conduct an in-depth PR Review." It runs as a fixed-point loop terminating when a full pass surfaces no blocking findings. Each cycle clears blocking and functionally-impacting findings (within reason — escalate edge cases). Cosmetic suggestions can be deferred.
Review State: llms/NOTES.md
llms/NOTES.md is the ephemeral per-review cycle tracker — lives in the llms/ directory alongside other LLM guidance, persists across cycles within one review but never across reviews. It holds the cycle counter, the Conducting review / Implementing suggestions phase toggles, and per-gate checkboxes; update as you advance. The canonical, committed source is llms/NOTES.template.md. When starting a review, instantiate from the template:
cp llms/NOTES.template.md llms/NOTES.md
If llms/NOTES.md is missing or stale, reset from the template; never assume prior cycle state carries over.
Rebase
(cd ../mcp-context-forge && git pull)
git rebase main
For each conflict, apply the resolution that best preserves both intents using
the PR diff and recent main commits as context. Escalate when the conflict is
semantic (logic intent on both sides), would silently weaken a test / security
check / migration, or when you're not confident the fix matches the PR author's
intent. Preserve sign-off (git commit -s) on any new commits.
For conflicts with .secrets.baseline, use the version of .secrets.baseline
from the main branch.
Cycle: review → fix → loop
- Verify scope. Cross-reference the PR description against any linked issues (
gh pr view,gh issue view) and confirm the changes deliver what the PR claims. Partial coverage of an issue is acceptable when the PR documentation explicitly says so; unstated gaps or scope drift are blocking findings. - Review. Default categorization: blocking / functionally-impacting / suggestions / minor (matches Tone for GitHub Comments). Output format and delivery channel (file, agent toolkit,
gh pr review) decided per-PR — confirm before posting externally. - Fix. Address every blocking and functionally-impacting finding this cycle. Update
llms/NOTES.mdto advance state. - Loop. Repeat until a full pass yields zero blocking findings, then run the validation gate.
Pre-Merge Validation Gate
Run from the worktree root, in order. Each must pass (or have a documented waiver) before the PR is ready:
| # | Command | Validates |
|---|---|---|
| 1 | make ruff interrogate pylint | Lint, docstring coverage, deeper static analysis |
| 2 | make test | Full pytest suite |
| 3 | make coverage diff-cover | Coverage of changed lines vs. base |
| 4 | make docker-nuke docker-prod-rust testing-up RUST_MCP_MODE= | Rebuilds and launches the production-style gateway stack |
| 5 | make test-mcp-protocol-e2e test-mcp-rbac | MCP protocol E2E and RBAC against the live gateway |
| 6 | make detect-secrets-scan | No new secrets in files changed vs main; exits non-zero on live/unaudited findings (jq merge preserves out-of-scope audited entries; remediate with make detect-secrets-audit) |
Distinct from the per-edit hygiene chain in Essential Commands → Code Quality (make autoflake isort black pre-commit, then make ruff bandit interrogate pylint verify): hygiene runs continuously; this gate runs once before declaring a PR ready.
Secret Detection (detect-secrets)
When detect-secrets identifies false positives:
- Python files: Suppress inline using
# pragma: allowlist secretcomment so they don't appear in.secrets.baselineafter runningmake detect-secrets-scan- The one exception to this is doctest strings where the content contains a false positive secret as the comment will interfere with assertions. In this case rely on the .secrets.baseline file and audit the result with 'make detect-secrets-audit'.
- All other file types: Regenerate the baseline using
make detect-secrets-scanto update.secrets.baseline
Authentication & RBAC Overview
ContextForge implements a two-layer security model:
- Token Scoping (Layer 1): Controls what resources a user CAN SEE (data filtering)
- RBAC (Layer 2): Controls what actions a user CAN DO (permission checks)
Token Scoping Quick Reference
API / legacy tokens — JWT teams claim is the sole authority (normalize_token_teams()):
JWT teams State | is_admin: true | is_admin: false |
|---|---|---|
| Key MISSING | PUBLIC-ONLY [] | PUBLIC-ONLY [] |
teams: null | ADMIN BYPASS | PUBLIC-ONLY [] |
teams: [] | PUBLIC-ONLY [] | PUBLIC-ONLY [] |
teams: ["t1"] | Team + Public | Team + Public |
Session tokens (token_use: "session") — DB is the authority; JWT teams only narrows (resolve_session_teams()):
JWT teams State | DB admin? | Result | Access Level |
|---|---|---|---|
| any | yes | None | ADMIN BYPASS (DB authority) |
Missing/null/[] | no | DB teams | Full DB membership |
["t1"] | no | intersection | Narrowed to overlap |
["revoked"] | no | [] | Public-only (fail-closed) |
Key behaviors:
- API/legacy tokens: Missing
teamskey = public-only access (secure default). Admin bypass requires BOTHteams: nullANDis_admin: true.normalize_token_teams()inmcpgateway/auth.pyis the single source of truth. - Session tokens: Admin bypass is determined by the DB
is_adminflag, not the JWTteamsclaim. Non-admin sessions can be narrowed via JWTteams.resolve_session_teams()inmcpgateway/auth.pyis the single policy point. - Layer 1 only: Token scoping controls visibility (what you can see). RBAC (Layer 2) is evaluated independently — session-token narrowing does not restrict which team roles are checked for permissions.
- External IdP tokens: identities provisioned from trusted external SSO providers (see
SSO_API_TOKEN_AUTH_ENABLED) are dispatched through the session-token table above (resolve_session_teams()), not the API/legacy table —is_admin/teamscome from the persisted local user record, never from the external token's claims.
Implementation Helpers
Layer-1 derivation is centralized in mcpgateway/auth_context.py. Route handlers must call these rather than re-deriving the rule inline:
get_scoped_resource_access_context(request, user)→(user_email, token_teams)— the visibility scope to pass into service fetch/list/read calls. Admin bypass is signalled bytoken_teams=Nonewhileuser_emailis kept, so the service can still owner-match the admin's own private rows. Public-only is(email, []).get_request_identity(request, user)→(user_email, is_admin)— the requester's own identity, for audit capture and header masking. Use when you need the identity independent of the visibility scope.get_rpc_filter_context(request, user)→(user_email, token_teams, is_admin)— the raw pre-rule triple. Reserved for the few sites that genuinely needis_adminor the un-normalized teams (auth-context forwarding, run-ownership capture, tool-execution authorization); these carry aLayer-1 exceptioncomment naming the reason.
The derived triple is memoized on request.state per principal, so calling the scope and identity helpers together costs one derivation rather than two.
Security Invariants (Required)
- Treat
publicas platform-public scope, not internet-anonymous scope. - Explicit exception: when
MCP_REQUIRE_AUTH=false, unauthenticated/mcprequests are allowed with public-only visibility — unless the target virtual server hasoauth_enabled=True, in which case unauthenticated requests are rejected with 401 regardless of the global setting. - Keep the two-layer model on every path:
- Layer 1: token scoping controls what a caller can see.
- Layer 2: RBAC controls what a caller can do.
- Do not re-implement token team interpretation logic; use
normalize_token_teams()for API/legacy tokens andresolve_session_teams()for session tokens (both inmcpgateway/auth.py). - Do not re-implement Layer 1 token scope semantics; use
token_scope_grants()inmcpgateway/middleware/rbac.py, the single policy point shared by the RBAC decorators andTokenScopingMiddleware. Empty token scopes mean "inherit from RBAC at runtime" (whatTokenCatalogService._generate_token()emits for tokens created without an explicit scope) and must never be treated as deny-all;*grants everything and<category>.*grants that category. - Do not accept inbound client auth tokens via URL query parameters.
- Legacy
INSECURE_ALLOW_QUERYPARAM_AUTHis interop-only for outbound peer auth and must remain opt-in and host-restricted. - High-risk transports must be feature-flagged and disabled by default.
- Transport/session endpoints must authenticate before session establishment (or message forwarding) and enforce RBAC before processing actions.
- Token-scoped route authorization must be default-deny for unmapped protected paths.
- Never trust client-provided ownership fields (
owner_email,team_id, session owner); derive authorization from authenticated identity and server-side state. - Security-sensitive changes must include deny-path regression tests (unauthenticated, wrong team, insufficient permissions, feature disabled).
- A
token-exchangeOAuth grant (RFC 8693 / On-Behalf-Of) exists for gateways; with it, the user's inbound JWT is exchanged with a trusted Authorization Server and never forwarded upstream — only the exchanged token is sent to the downstream MCP server. token_urlon atoken-exchangegateway is an SSRF / egress boundary: the user's ContextForge JWT is POSTed to it as thesubject_token, it is validated at config time, and creating or modifying token-exchange gateways is a privileged action.- Audit token-exchange operations via the structured logging sink with a
correlation_id; never log raw subject tokens or exchanged tokens.
UAID Cross-Gateway Security
- UAID cross-gateway routing requires explicit domain allowlist (fail-closed default)
- Empty
UAID_ALLOWED_DOMAINSblocks all cross-gateway routing unlessUAID_ALLOW_ALL_DOMAINS=true - Cross-gateway calls forward bearer tokens for RBAC enforcement on remote gateways
- Both gateways must trust the same JWT issuer (shared
JWT_SECRET_KEYor federated SSO) UAID_ALLOW_ALL_DOMAINS=trueis unsafe for production (bypasses allowlist validation)- Startup validation logs ERROR if A2A enabled but allowlist not configured
- Remote gateway 401/403 responses raise
A2AAgentErrorwith troubleshooting guidance
Built-in Roles
| Role | Scope | Key Permissions |
|---|---|---|
platform_admin | global | * (all) |
team_admin | team | teams.*, tools.read/execute, resources.read |
developer | team | tools.read/execute, resources.read |
viewer | team | tools.read/execute, resources.read |
Documentation
- Full RBAC guide:
docs/docs/manage/rbac.md - Multi-tenancy architecture:
docs/docs/architecture/multitenancy.md - OAuth token delegation:
docs/docs/architecture/oauth-design.md
User Identity Extraction
Canonical Email Precedence: All user-email extraction helpers use a consistent email-over-sub precedence order to ensure forensic accuracy across visibility checks and audit logs:
- When a user dict contains both
emailandsubkeys,emailtakes precedence - The canonical implementation is
get_user_email()inmcpgateway/auth_context.py - All other helpers (including
admin.get_user_email) re-export or delegate to this canonical implementation - This ensures that the identity used for RBAC evaluation matches the identity logged in audit trails
Rationale: The email field is the human-readable identifier used throughout AGENTS.md and user-facing documentation. Consistent precedence prevents forensic confusion where an incident review pivots on a logged email that differs from the principal actually evaluated by RBAC.
Observability Transaction Behavior
Issue #3883 - Separate Session Pattern
Observability write operations use independent database sessions that commit immediately (best-effort pattern). This means:
- Observability data persists even when the main request fails
- Traces may show "in progress" or partial states for failed requests
- NOT atomic with main request transaction (intentional trade-off)
- Provides visibility into partial failures at the cost of atomicity
Implementation Details
Write methods (use independent sessions):
start_trace(),end_trace()start_span(),end_span()add_event(),record_token_usage(),record_metric(),delete_old_traces()
Query methods (use request-scoped sessions):
get_trace(),get_traces(),get_spans(), etc.- These accept a
db: Sessionparameter for RBAC/token scoping
Context managers (create single independent session for lifecycle):
trace_span(),trace_tool_invocation(),trace_a2a_request()
Pattern: Follows existing SQL instrumentation approach in instrumentation/sqlalchemy.py:58-87
Audit Trail Transaction Behavior
Issue #2871 - Separate Session Pattern
AuditTrailService.log_action() (mcpgateway/services/audit_trail_service.py) always opens its own SessionLocal() when no db is supplied, and closes/rolls back that session itself. Callers in tool_service.py, resource_service.py, gateway_service.py, prompt_service.py, server_service.py, and admin.py must never pass db=db (the caller's request-scoped session) to log_action(). In admin.py, plugin-view audit logging goes through log_audit(), which is a thin wrapper over log_action() and inherits the same optional-session behavior.
Passing the shared session caused "This transaction is inactive" errors: the main CRUD operation already calls db.commit() before the audit call, and reusing that same session for a second commit after it has already committed leaves the session in a state that breaks rollback on subsequent errors.
log_action()swallows its own exceptions internally and returnsNoneon failure — it does not propagate them to the caller in production.- The main resource (tool/gateway/resource/prompt/server) is committed before
log_action()runs, so an audit-logging failure never rolls back already-persisted data — but callers' genericexcept Exceptionhandlers still calldb.rollback()and surface an error to the API caller even though the underlying row was already committed. - Do not add
db: Sessionback to alog_action()call site. If a service needs the audit entry guaranteed within the same transaction as the main write, that is a deliberate design change requiring review, not a default.
Middleware: ObservabilityMiddleware no longer creates request.state.db. Each observability operation creates its own short-lived session.
Security: Query operations use request-scoped sessions for RBAC/token scoping. Write operations are not RBAC-protected (observability visibility is platform-wide).
Connection Pool Sizing: The separate session pattern creates 4-6 independent database sessions per traced request (trace start/end, span start/end, metrics, events). Default configuration (DB_POOL_SIZE=200, DB_MAX_OVERFLOW=10) provides 210 total connections, supporting ~35 concurrent traced requests. This is adequate for typical deployments. High-traffic production systems (>50 req/sec sustained) should increase pool size via environment variables: DB_POOL_SIZE=500, DB_MAX_OVERFLOW=100 to support 80+ concurrent requests. Monitor for "QueuePool limit exceeded" errors and adjust pool sizing accordingly. Note: SQLite connections are capped at 50 due to file-based limitations.
Key Environment Variables
Defaults come from mcpgateway/config.py. .env.example intentionally overrides a few for local/dev convenience.
# Core
HOST=127.0.0.1 # .env.example uses 0.0.0.0
PORT=4444
DATABASE_URL=sqlite:///./mcp.db # or postgresql+psycopg://...
REDIS_URL=redis://localhost:6379/0
RELOAD=false
# Auth
JWT_SECRET_KEY=your-secret-key
BASIC_AUTH_USER=admin
BASIC_AUTH_PASSWORD=changeme
AUTH_REQUIRED=true # Set false ONLY for development
AUTH_ENCRYPTION_SECRET= # REQUIRED: generate with: make init-secrets-patch-env
# Features
MCPGATEWAY_UI_ENABLED=false # .env.example sets true
MCPGATEWAY_ADMIN_API_ENABLED=false # .env.example sets true
MCPGATEWAY_A2A_ENABLED=true
PLUGINS_ENABLED=false
PLUGINS_CONFIG_FILE=plugins/config.yaml
# Logging
LOG_LEVEL=ERROR
LOG_TO_FILE=false
STRUCTURED_LOGGING_DATABASE_ENABLED=false
# Observability
OBSERVABILITY_ENABLED=false
OTEL_EXPORTER_OTLP_ENDPOINT= # .env.example sets http://localhost:4317
MCP Helpers
# Generate JWT token
python -m mcpgateway.utils.create_jwt_token --username admin@example.com --exp 10080 --secret KEY
# Export for API calls
export MCPGATEWAY_BEARER_TOKEN=$(python -m mcpgateway.utils.create_jwt_token --username admin@example.com --exp 0 --secret KEY)
# Expose stdio server via HTTP/SSE
python -m mcpgateway.translate --stdio "uvx mcp-server-git" --port 9000
Adding an MCP Server
- Start:
python -m mcpgateway.translate --stdio "server-command" --port 9000 - Register:
POST /gateways - Create virtual server:
POST /servers - Access via SSE/WebSocket endpoints
Technology Stack
- FastAPI with Pydantic validation and SQLAlchemy ORM (Starlette ASGI)
- HTMX + Alpine.js for admin UI
- SQLite default, PostgreSQL support, Redis for caching/federation
- Alembic for migrations
Synchronous SQLAlchemy in Async Handlers (Design Decision)
The codebase deliberately uses synchronous SQLAlchemy sessions (e.g. SessionLocal().begin()) inside async FastAPI handlers and ASGI middleware, relying on FastAPI's event-loop management to handle blocking operations. Do not flag this as a bug or attempt to convert individual call sites to async without a broader migration plan. This design decision may be revisited in the future alongside a potential migration to async database drivers.
Alembic Database Migrations
When adding new database columns or tables, create an Alembic migration.
Creating Migrations
# CRITICAL: Always check the current head FIRST
cd mcpgateway && alembic heads
# Generate a new migration (auto-generates from model changes)
alembic revision --autogenerate -m "add_column_to_table"
# Or create an empty migration for manual edits
alembic revision -m "add_column_to_table"
Migration File Requirements
The down_revision MUST point to the current head. Never guess or copy from older migrations.
# CORRECT: Points to actual current head (verified via `alembic heads`)
revision: str = "abc123def456"
down_revision: Union[str, Sequence[str], None] = "43c07ed25a24" # Current head
# WRONG: Creates multiple heads (breaks all tests)
down_revision: Union[str, Sequence[str], None] = "some_old_revision"
Idempotent Migrations Pattern
Always write idempotent migrations that check before modifying:
def upgrade() -> None:
inspector = sa.inspect(op.get_bind())
# Skip if table doesn't exist (fresh DB uses db.py models directly)
if "my_table" not in inspector.get_table_names():
return
# Skip if column already exists
columns = [col["name"] for col in inspector.get_columns("my_table")]
if "new_column" in columns:
return
op.add_column("my_table", sa.Column("new_column", sa.String(), nullable=True))
Verification
# Verify single head after creating migration
cd mcpgateway && alembic heads
# Should show only ONE head
# Run tests to confirm migrations work
make test
Common Errors
- "Multiple heads are present": Your
down_revisionpoints to wrong parent. Fix by updating to actual current head. - "Target database is not up to date": Run
alembic upgrade headfirst.
Hermetic Downgrade: Config Snapshot Pattern
Any migration whose downgrade() logic depends on runtime configuration (i.e., reads from
mcpgateway.config.settings) must snapshot those values into migration_metadata during
upgrade() and read them back during downgrade(). This makes the migration hermetic —
its behaviour is determined by database state, not the current environment.
from mcpgateway.config import settings
from sqlalchemy import inspect, text
REVISION = "your_revision_id"
def upgrade() -> None:
bind = op.get_bind()
# ... schema changes ...
# Snapshot any settings values used in downgrade
if "migration_metadata" in inspect(bind).get_table_names():
bind.execute(
text(
"INSERT INTO migration_metadata (revision, key, value, created_at) "
"VALUES (:rev, :key, :val, CURRENT_TIMESTAMP) "
"ON CONFLICT (revision, key) DO UPDATE SET value = excluded.value"
),
{"rev": REVISION, "key": "some_setting", "val": settings.some_setting},
)
def downgrade() -> None:
bind = op.get_bind()
# Read from snapshot; fall back to live settings only if table is absent
# (pre-existing DB that was upgraded before this pattern was introduced)
cfg = {}
if "migration_metadata" in inspect(bind).get_table_names():
rows = bind.execute(
text("SELECT key, value FROM migration_metadata WHERE revision = :rev"),
{"rev": REVISION},
).all()
cfg = {r[0]: r[1] for r in rows}
some_setting = cfg.get("some_setting") or settings.some_setting
# ... use some_setting for downgrade logic ...
# Clean up snapshot rows
if "migration_metadata" in inspect(bind).get_table_names():
bind.execute(
text("DELETE FROM migration_metadata WHERE revision = :rev"),
{"rev": REVISION},
)
Rule: If your migration imports settings and uses it in downgrade(), you must follow this
pattern. Migrations that only use settings in upgrade() (e.g., for seeding initial data) are
exempt.
Coding Standards
- Python >= 3.11 with type hints; strict mypy
- Formatting: Ruff (line length 200)
- Linting: Ruff (
E3,E4,E7,E9,F,D1), Pylint perpyproject.toml - Naming:
snake_casefunctions/modules,PascalCaseclasses,UPPER_CASEconstants - Imports: Group per isort sections (stdlib, third-party, first-party
mcpgateway, local)
Commit & PR Standards
- Sign commits:
git commit -s(DCO requirement) - Conventional Commits:
feat:,fix:,docs:,refactor:,chore: - Link issues:
Closes #123 - Include tests for behavior changes
- Require green lint and tests before PR
- Don't push until asked.
Tone for GitHub Comments
When posting PR reviews, issue comments, or any public-facing text on GitHub, use a collaborative and constructive tone:
- Lead with what's good before raising concerns.
- Frame issues as questions or options ("worth considering", "a couple of approaches") rather than directives.
- Remember contributors are people doing their jobs — be direct about problems without being harsh.
- Categorize findings clearly (blocking, suggestions, minor notes) so the author knows what must change vs. what's optional.
- Avoid sounding algorithmic or robotic; write the way a respectful senior colleague would in a code review.
GitHub Issues (Brief)
- Prefer issue templates in
.github/ISSUE_TEMPLATE/:bug-report-code.md,feature-request.md,docs-issue.md,testing--bug--unit--manual--or-new-test-.md,chore-task--devops--linting--maintenance-.md. - Title style should include type prefix, for example:
[BUG]: ...,[FEATURE]: ...,[DOCS]: ...,[TESTING]: ...,[CHORE]: .... - Label baseline: one primary type label (
bugorenhancementordocumentationortestingorchore) plustriageon new issues. - Add 1-3 optional scope labels as needed (for example
security,performance,ui,api,python,devops,a2a,mcp-protocol). - Epic title format:
[EPIC][SECURITY]: Security clearance levels plugin - Bell-LaPadula MAC implementation #1245. - Epic labels:
epic,security,enhancement,triage(plus optional scope labels).
Maintenance Guardrails (Brief)
- Source of truth precedence:
mcpgateway/config.pyand runtime code >Makefiletargets/dependencies >.env.example(dev overrides) > docs/comments. - When auditing repo state, prioritize active source directories and ignore transient/workbench content unless explicitly requested:
todo/,tmp/,artifacts/,logs/,coverage/. - Issue lifecycle labels: use
awaiting-userwhen blocked on reporter feedback,blockedfor dependency blockers,plannedwhen accepted but deferred, andfixedonly after the resolving change is merged. - Avoid brittle numeric claims (counts of services/routers/middleware/plugins) unless you are actively validating and updating them in the same change; otherwise describe with approximate wording.
Important Constraints
- Never mention AI assistants in PRs/diffs
- Do not include test plans or effort estimates in PRs
- Never create files unless absolutely necessary; prefer editing existing files
- Never proactively create documentation files unless explicitly requested
- Never commit secrets; use
.envfor configuration
Key Files
README.md- Canonical project overview and quick startmcpgateway/main.py- Application entry pointmcpgateway/config.py- Environment configurationmcpgateway/db.py- SQLAlchemy ORM models and session managementmcpgateway/schemas.py- Pydantic schemaspyproject.toml- Project configurationMakefile- Build automation.env.example- Environment template
CLI Tools Available
ghfor GitHub operationsmakefor build/test automationuvfor virtual environment management and foruv tool runlinter invocations- Dev-group tools installed in the venv:
pytest,mypy,bandit,pre-commit, etc. (seepyproject.toml[dependency-groups]) - Formatters and linters (
ruff,vulture,interrogate,radon,yamllint,tomlcheck) are pinned in theMakefileand invoked on demand viauv tool run; always prefer the Makefile targets (make black,make ruff, etc.) over calling the underlying tools directly