Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/joseluis-dev/harness-ai/llms.txt

Use this file to discover all available pages before exploring further.

Phase 3 introduces the first real institutional agent in the harness: a natural-language SQL interface over pre-approved database views. The key architectural insight of this phase is that mcp-readonly-sql is not a special-cased component hard-wired into harness-core — it is a seed row in the tool_registry, consumed by the sql_readonly_agent exactly like any other registered tool. The core never enumerates MCPs in code; it resolves them by name from the registry at runtime. This phase is therefore the proof-of-concept for the entire tool-registry model that all subsequent phases reuse.

Objective

Allow an institutional user to ask questions in natural language about pre-approved SQL views, have the agent generate valid SQL, execute it safely, and respond with an explanation — without ever permitting writes, exposing raw tables, or hard-coding the MCP list into the core codebase.

Dependencies

  • Phase 0 (Base executable local) — PostgreSQL, harness_db, harness_auth_db, executions, and audit_events must be running.
  • Phase 0b (Platform bootstrap) — the tool_registry table and mcp-auth-service must be operational.
  • Phase 1 (External identity) — mcp-identity-connector must be available to attribute every query to a real institutional user.
  • Phase 2 (Model Gateway) — the POST /v1/chat endpoint is used by the agent to generate SQL from natural language.

mcp-readonly-sql: Service Specification

Allowed Connection Profiles and Query Constraints

{
  "allowed_connection_profiles": ["harness_readonly"],
  "reject_multi_statement": true,
  "statement_allowlist": ["SELECT"],
  "blocklist": [
    "INSERT", "UPDATE", "DELETE", "DROP",
    "ALTER", "TRUNCATE", "CREATE", "REPLACE",
    "MERGE", "CALL", "EXEC"
  ],
  "max_rows": 500,
  "query_timeout_ms": 5000,
  "response_size_limit_bytes": 1048576
}

Allowlisted Views

The sql_readonly_agent may only generate SQL against views in the approved catalog. The initial seed includes:
View nameDescription
vw_expedientes_resumenSummary of active institutional files (expedientes)
vw_tramites_estadoCurrent status of administrative procedures (trámites)
Additional views are added to the catalog by updating the tool_registry seed — no code change is required.

Readonly Database User

mcp-readonly-sql connects to harness_db using a PostgreSQL user that holds only SELECT privilege on the allowlisted views. This user has no access to raw tables, system catalogs, or any other schema object. The connection string is loaded from the service’s own environment — it is never passed from the agent or from user input.

Authentication

All HTTP requests to mcp-readonly-sql must carry an Authorization: Bearer <access_token> header. The service validates tokens using auth-sdk against mcp-auth-service. Tokens without the required scope are rejected with 401 WWW-Authenticate.
RequirementDetail
Required scopesql:read (for executing queries)
Additional scopessql:list_views, sql:describe_view
Token locationAuthorization header only — never query string
Invalid / expired token401 with WWW-Authenticate and resource metadata

tool_registry Seed Row

mcp-readonly-sql enters the harness as a registry entry, not as a hard-coded component. The seed row is applied at Phase 3 startup:
INSERT INTO tool_registry (
  technical_name,
  visible_name,
  type,
  transport,
  endpoint,
  state,
  risk_level,
  required_scopes,
  max_rows,
  query_timeout_ms,
  response_size_limit_bytes,
  reject_multi_statement,
  requires_human_confirmation
) VALUES (
  'mcp-readonly-sql',
  'SQL Readonly (vistas institucionales)',
  'mcp_http',
  'oauth',
  'http://mcp-readonly-sql:8010',
  'active',
  'READ_ONLY',
  ARRAY['sql:read', 'sql:list_views', 'sql:describe_view'],
  500,
  5000,
  1048576,
  true,
  false
);
harness-core never references mcp-readonly-sql by name in application code. The sql_readonly_agent resolves the tool by its technical_name from the registry, receives a descriptor and an access_token with scope=sql:read, and delegates to the authenticated HTTP gateway.

Agent Flow

User (natural language question)
  → sql_readonly_agent
      → tool_registry.resolve("mcp-readonly-sql")
          → mcp-auth-service: obtain access_token (scope=sql:read)
      → Model Gateway: generate SQL from natural language
          → POST /v1/chat (vllm_local)
      → POST http://mcp-readonly-sql:8010/query
          Authorization: Bearer <access_token>
          → SQL parser: validate against allowlist + blocklist
          → PostgreSQL (harness_readonly user): execute on allowlisted view
          → enforce max_rows, query_timeout, response_size_limit
      → Model Gateway: generate natural language explanation of result
  → User (explanation + structured data)

Acceptance Criteria

1

mcp-readonly-sql active in tool_registry — no hardcoded enumeration

curl -fsS http://localhost:4321/api/admin/tools?technical_name=mcp-readonly-sql
Expected — a single row with state=active and the three required scopes. Confirm with a grep that harness-core source code contains no string literal "mcp-readonly-sql" in application logic (seed scripts and tests are exempt).
2

All HTTP requests carry Authorization Bearer — never query string

Review access logs for mcp-readonly-sql. Confirm every incoming request has an Authorization: Bearer header and that no token appears in request.url query parameters.
3

Invalid tokens return 401 with WWW-Authenticate

curl -fsS -X POST http://mcp-readonly-sql:8010/query \
  -H "Authorization: Bearer invalid-token" \
  -d '{"sql": "SELECT * FROM vw_expedientes_resumen LIMIT 5"}'
Expected — HTTP 401 with a WWW-Authenticate header identifying the protected resource and the required scope.
4

User asks in natural language and agent responds

Submit a question through the chat UI or the API:
curl -fsS -X POST http://localhost:4321/api/chat \
  -H "Content-Type: application/json" \
  -H "X-Harness-Actor-Id: jlopez" \
  -d '{
    "agent": "sql_readonly_agent",
    "message": "¿Cuántos expedientes están en estado pendiente?"
  }'
Expected — HTTP 200 with a natural language explanation and the supporting data from vw_expedientes_resumen.
5

Agent generates SQL only against allowlisted views

Inspect the audit_events entries created during the chat call above. The sql field in each mcp.call event must reference only views present in the allowlist. No raw table name (expedientes, tramites, etc.) may appear in generated SQL.
6

Blocklisted statements rejected before execution

# Attempt a write via the MCP endpoint directly
curl -fsS -X POST http://mcp-readonly-sql:8010/query \
  -H "Authorization: Bearer <valid_sql_read_token>" \
  -H "Content-Type: application/json" \
  -d '{"sql": "DELETE FROM vw_expedientes_resumen WHERE 1=1"}'
Expected — HTTP 400 (or 403) with error: STATEMENT_BLOCKED. The audit record must show decision=POLICY_DENIED.
7

Query enforces max_rows, timeout, and response size

Issue a query designed to return more than 500 rows. Confirm the response contains at most 500 rows and includes a truncated: true indicator. Confirm that a slow query is terminated at 5000 ms with a timeout error stored in audit_events.
8

Response includes explanation — not just raw data

The sql_readonly_agent must return a human-readable explanation alongside any query result. A response that returns only a raw JSON array without explanation fails this criterion.
9

Write attempts blocked and recorded as POLICY_DENIED

Attempt INSERT, UPDATE, DELETE, DROP, and TRUNCATE statements through any available path (direct MCP call, agent chat, API). All must produce POLICY_DENIED in audit_events.
10

Agent never delivers connection strings — credentials resolved by the MCP

Review the full request log between sql_readonly_agent and mcp-readonly-sql. The payload must never contain a connection string, username, password, or database host. The MCP resolves its own credentials from environment or the secret manager; the auth-sdk is used exclusively to validate Bearer token authorization.

Operational Notes

The risk_level=READ_ONLY designation permits omitting the human-confirmation gate for listed operations. Any attempt to elevate a query’s risk_level to READ_SENSITIVE or higher activates the Phase 9 human-approval flow. Guard against agents that try to negotiate a higher risk level.
  • The tool_registry is the runtime source of truth. The contract matrix in contracts/04-mcp-contracts.md is a historical reference; never override registry entries with hardcoded values.
  • The mcp-readonly-sql seed scripts and mcp-auth-service descriptor scripts must be executed during Phase 3 startup, before any agent call is made.
  • The chat UI introduced here is the first conversational UI in the harness. It serves as the foundation for the document_rag_agent UI in Phase 5.
  • Multi-statement SQL (SELECT 1; SELECT 2) is rejected by the parser regardless of content. reject_multi_statement: true is not a heuristic — it is a structural block applied before any statement is evaluated.

Build docs developers (and LLMs) love