Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/standalone-product.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ jobs:
run: |
python -m unittest discover -s tests/backend -p test_selection_policy.py -v
python -m unittest discover -s tests/backend -p test_fresh_policy_writes.py -v
mcp-adapter:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install the MCP server package
run: python -m pip install --disable-pip-version-check ./mcp-server
- name: Test MCP tools through in-memory and stdio MCP client sessions
run: python -m unittest discover -s tests/mcp -p 'test_*.py' -v
- name: Install only pinned API-test dependencies
run: python -m pip install --disable-pip-version-check fastapi==0.150.0 pydantic==2.14.1
- name: Verify session continuation stays on its own agent
run: python -m unittest discover -s tests/backend -p test_session_scope.py -v
evaluation-reliability:
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
**/*.log
logs/
backend/logs/
mcp-server/build/
**/*.egg-info/
.pytest_cache/
.ruff_cache/
.coverage
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Open source · Apache-2.0 · Self-hostable
- **Use the API or customise the application.** Call the same agents from your
own software through the [direct product API](docs/api.md), or change the
open-source implementation and its [model configuration](docs/model-configuration.md).
- **Ask from Claude Code and other MCP clients.** The included
[MCP server](docs/mcp.md) exposes your agents as tools: ask a question and get
the answer, SQL, rows and chart metadata, continue the conversation, or read
saved results.

![Database Agent answering a synthetic sales question](docs/images/sales-overview.png)

Expand Down Expand Up @@ -152,6 +156,7 @@ you intend to erase this installation's data.

- [Configuration and authentication](docs/configuration.md)
- [Direct product API and OpenAPI reference](docs/api.md)
- [MCP server for Claude Code and other MCP clients](docs/mcp.md)
- [AWS deployment: ECS/Fargate and static UI](docs/aws-deployment.md)
- [Development and reproducible evaluation](docs/development.md)
- [Architecture](docs/architecture.md) and [source layout](docs/source-layout.md)
Expand Down
3 changes: 2 additions & 1 deletion backend/api/v1/routers/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
)
from agent.runner import run_agent_flow
from api.v1.utils.chat_helper import (
get_past_messages_safe, get_owner_email_by_agent_id,
get_past_messages_safe, get_owner_email_by_agent_id, ensure_session_belongs_to_agent,
get_agent_with_visibility, get_agent_runtime_db,
get_chat_log_for_export, get_session_messages_data,
build_chat_response, maybe_upload_csv_export, persist_chat_logs,
Expand Down Expand Up @@ -217,6 +217,7 @@ async def chat_with_agent(

chat_id = str(uuid.uuid4())
session_id = body.session_id
await ensure_session_belongs_to_agent(session_id, agent_id)

message_history = await get_past_messages_safe(session_id, user_id, agent_id, body.query)
past_messages = message_history["past_messages"]
Expand Down
14 changes: 14 additions & 0 deletions backend/api/v1/utils/chat_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ async def get_past_messages_safe(session_id, user_id, agent_id, user_query):
return {"past_messages": [], "session_id": session_id}


async def ensure_session_belongs_to_agent(session_id: Optional[str], agent_id: str) -> None:
"""Refuse to continue a session that was not created on this agent.

History is read by session_id alone (get_past_conversation_safe), so an unchecked id would
replay another agent's questions and answers into this agent's model context and append the
new turn to that foreign session. A new conversation (no id) is always allowed.
"""
if not session_id or session_id in ["", "string"]:
return
from session.db_utils.session_db_utils import get_session_details
if not await get_session_details(session_id, agent_id):
raise HTTPException(status_code=404, detail=f"Session {session_id} not found for agent {agent_id}")


async def get_owner_email_by_agent_id(agent_id: str) -> Optional[str]:
try:
async for session in get_async_session():
Expand Down
7 changes: 7 additions & 0 deletions backend/session/db_utils/session_db_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,13 @@ async def get_session_data(session_id: str):
# The ONLY change to this payload: the chart is sent instead of a
# hardcoded None. Every other key, debug_info included, is untouched.
assistant_message["graph_output"] = _chart_for_replay(row_obj)
# Additive: the column-labelled result block stored for this turn
# ({columns, rows, row_count, truncated}); `db_output` above keeps bare rows
# without column names. None for turns stored before the block existed.
stored_debug = getattr(row_obj, "debug_info", None)
assistant_message["result"] = (
stored_debug.get("db_result") if isinstance(stored_debug, dict) else None
)
session_data.append(assistant_message)
logger.info(f"Returning {len(session_data)} total messages for session {session_id}")
return session_data
Expand Down
6 changes: 5 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Local API root: `http://127.0.0.1:18000/nl2sql/api`.
Interactive reference: [/nl2sql/api/docs](http://127.0.0.1:18000/nl2sql/api/docs).
Machine contract: [/nl2sql/api/openapi.json](http://127.0.0.1:18000/nl2sql/api/openapi.json).
The UI calls the same product API through its same-origin proxy.
The UI calls the same product API through its same-origin proxy. MCP clients
such as Claude Code can call it through the [MCP server](mcp.md).

Trusted operator clients use the private `FASTAPI_SECRET_KEY` as `X-API-Key`.
The agent-key endpoint issues a narrower agent credential for chat. Keep both
Expand All @@ -19,6 +20,9 @@ uses the signed HTTP-only session cookie; no hosted Auth service is required.
| `POST /v1/agents/{id}/databases/{db_id}/tables` | Supply table descriptions/visibility |
| `POST /v1/agents/{id}/api-keys` | Issue an agent key; never log the returned key |
| `POST /v1/agents/{id}/chat?stream=false` | Direct question with JSON response |
| `POST /v1/agents/{id}/sessions/{session_id}/messages?stream=false` | Follow-up in a conversation; the session must belong to this agent |
| `GET /v1/agents/{id}/sessions` | Saved conversations on the agent |
| `GET /v1/agents/{id}/sessions/{session_id}` | A saved conversation, each answer with its SQL, `result` rows and chart |

Use the generated OpenAPI contract for complete request/response fields and
streaming options. `tests/evaluation/run.py` is a small working client example:
Expand Down
165 changes: 165 additions & 0 deletions docs/mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# MCP server

`mcp-server/` lets MCP-compatible AI applications, such as Claude Code, use your
Database Agent agents as tools. It is a thin adapter over the
[direct product API](api.md): every tool call is an ordinary API request made
with an existing Database Agent credential. Authentication, agent-key scope,
query limits, model configuration, table visibility, SQL access controls and
query execution all stay in the API. The adapter has no database credentials and
does not generate or run SQL.

## Tools

| Tool | What it does | API route |
|---|---|---|
| `list_agents` | Agents these credentials can use, with connected databases (name, engine, description, table count; never host, user or password) | `GET /v1/agents`, `GET /v1/agents/{id}`, `GET /v1/agents/{id}/databases` |
| `ask_database` | Ask a question; returns the answer, generated SQL, column-labelled rows, chart suggestion and metadata, and a `session_id` | `POST /v1/agents/{id}/chat?stream=false` |
| `ask_database` with `session_id` | Follow-up question using that conversation's context | `POST /v1/agents/{id}/sessions/{session_id}/messages` |
| `list_conversations` | Saved conversations on an agent, newest first | `GET /v1/agents/{id}/sessions` |
| `get_conversation` | A saved conversation: each turn's question, answer, SQL, rows and chart | `GET /v1/agents/{id}/sessions/{session_id}` |

Every tool returns MCP structured content with a published output schema.
`max_rows` (default 100, up to 1000) limits only the rows copied into the tool
result; `row_count` and `truncated` report the full result. SQL and rows are
returned only when the agent's own settings allow them (`show_sql_query`,
`show_db_result`); otherwise the result says so in `notes`. Errors from the API
are returned as MCP tool errors with the server's reason, for example
`Not permitted for agent NL2SQL_… (HTTP 403): This API key is not valid for this agent`.

## Install

You need Python 3.10 or later and a running Database Agent API (the
[local installation](../README.md#install) or your own deployment).

```sh
pip install "git+https://github.com/FutureSmart-AI/database-agent.git#subdirectory=mcp-server"
```

This installs the `database-agent-mcp` command. From a clone you can instead run
`pip install ./mcp-server`, or skip installing with
`uvx --from ./mcp-server database-agent-mcp`.

## Credentials

Use per-agent API keys. A key works only on the agent it was issued for and
cannot manage keys, collaborators, the database connection or the agent itself.
Issue one in the UI under **Share → API**, or with the operator key:

```sh
curl -s -X POST "http://127.0.0.1:18000/nl2sql/api/v1/agents/$AGENT_ID/api-keys" \
-H "X-API-Key: $FASTAPI_SECRET_KEY" -H "Content-Type: application/json" -d '{}'
```

| Setting | Purpose |
|---|---|
| `DATABASE_AGENT_URL` | API root; default `http://127.0.0.1:18000/nl2sql/api`. Plain HTTP is refused for non-loopback hosts unless `DATABASE_AGENT_ALLOW_INSECURE_HTTP=1` |
| `DATABASE_AGENT_AGENT_KEYS` | `AGENT_ID=KEY` pairs, comma- or newline-separated. Recommended |
| `DATABASE_AGENT_API_KEY` | Operator (master) key, `FASTAPI_SECRET_KEY`. Reaches every agent; only for a trusted single-operator installation |
| `DATABASE_AGENT_TIMEOUT_SECONDS` | Per-request timeout; default 180 |

If both are set, an agent's own key is used for that agent and the operator key
for the rest. The server reads these from its environment and never prints them.

## Claude Code

Add a `.mcp.json` to your project. Claude Code expands the environment variables
when it starts the server, so the file holds no key:

```json
{
"mcpServers": {
"database-agent": {
"command": "database-agent-mcp",
"env": {
"DATABASE_AGENT_URL": "${DATABASE_AGENT_URL:-http://127.0.0.1:18000/nl2sql/api}",
"DATABASE_AGENT_AGENT_KEYS": "${DATABASE_AGENT_AGENT_KEYS}"
}
}
}
}
```

Set `DATABASE_AGENT_AGENT_KEYS` (for example `NL2SQL_1A2B3C4D=<key>`) in the
shell that starts Claude Code. To register the server for every project instead,
use `claude mcp add`; it stores the values you pass in your Claude Code
configuration file:

```sh
claude mcp add database-agent --scope user \
-e DATABASE_AGENT_URL=http://127.0.0.1:18000/nl2sql/api \
-e "DATABASE_AGENT_AGENT_KEYS=$AGENT_ID=$AGENT_KEY" \
-- database-agent-mcp
```

`claude mcp list` should show `database-agent: database-agent-mcp - ✔ Connected`.
Then ask in Claude Code, for example with the agent from
[Try the sample](../README.md#try-the-sample):

> Using the database-agent tools, what is the total amount for each region in
> orders? Show the SQL and rows. Then ask a follow-up in the same conversation:
> which region has the higher total?

Claude Code calls `list_agents`, then `ask_database`, then `ask_database` again
with the returned `session_id`. The expected rows are **North: 250.00** and
**South: 200.00**, and the follow-up answers North. Claude Code asks permission
before using each tool unless you allow it.

Other MCP clients use the same command and environment variables over stdio.

## The API alternative

The tools add no capability the API lacks. To call the agent directly:

```sh
curl -s -X POST "http://127.0.0.1:18000/nl2sql/api/v1/agents/$AGENT_ID/chat?stream=false" \
-H "X-API-Key: $AGENT_KEY" -H "Content-Type: application/json" \
-d '{"query": "What is the total amount for each region in orders?", "debug_info": true}'
```

The response carries `response`, `session_id`, `chat_id`, `is_answered`,
`debug_info.sql_query`, `debug_info.db_result` (`columns`, `rows`, `row_count`)
and `chart_output`. Send a follow-up to
`POST /v1/agents/{id}/sessions/{session_id}/messages` with the same body. See
[api.md](api.md) and the OpenAPI reference for every field.

## Security notes

- The agent's source credentials should be a dedicated read-only database
account. The adapter cannot widen what that account or the agent's table
visibility allows; it only forwards questions.
- A conversation can be continued only on the agent that created it: a
`session_id` from another agent returns HTTP 404 before any history is read
or any model is called. Deployments running an API version older than this
change do not perform that check.
- Saved conversations replayed through the API include SQL and rows regardless
of the agent's visibility settings. The adapter applies the agent's
`show_sql_query` and `show_db_result` to them, matching live answers, but a
caller using the API directly with the same key can read them.
- An agent key can list and read every conversation on its own agent, not only
the ones it created.

## Verification

`tests/mcp` checks the adapter through real MCP client sessions, in memory and
over stdio, against a synthetic API with the product's credential rules. CI runs
it with `tests/backend/test_session_scope.py`, which covers the session check
and saved-result replay in the API itself.

The integration was also run end to end on a disposable installation of this
branch (Compose stack, default model profile, the synthetic `sample_sales`
database with its read-only account) with two clients: the MCP Python SDK over
stdio, and Claude Code (`claude -p` with the `.mcp.json` above). That run covered:

- listing agents, asking with SQL, rows and a bar-chart suggestion, a follow-up
in the same session, and reading the saved conversation (North 250.00,
South 200.00);
- an agent configured to hide SQL and rows, live and saved;
- authorization failures: an unknown key (401), another agent's key (403), a
session from another agent (404) and an agent with no configured key;
- read-only access: requests to delete, drop or update data produced no SQL,
and the database still held its four original rows afterwards; a table the
database account can read but the agent's table visibility excludes was not
queried.

Charts are produced when the question asks for one ("Show a bar chart") and the
agent's chart setting allows it; `include_chart` sets that setting per question.
18 changes: 15 additions & 3 deletions docs/source-preservation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1100,9 +1100,9 @@
},
"backend/api/v1/routers/chat.py": {
"baseline_sha256": "2649938a3a23278b9a3ad952853436987b27e2918787af6b813b272059a47d78",
"current_sha256": "754fbc5e7afe30b250cd4926850d6ccff55d80537bd6357d6fef563f77585c0d",
"issue": "DA-011",
"reason": "Central versioned model configuration and explicit nonsecret configuration error response; existing prompts and SQL/security policy semantics preserved. Runtime acceptance pending."
"current_sha256": "cf916be23d558e1b8da82355f21e2cff7ae87f04ee4cfa3a496242c24c192642",
"issue": "DA-011 / DA-MCP-001",
"reason": "Central versioned model configuration and explicit nonsecret configuration error response; existing prompts and SQL/security policy semantics preserved. Runtime acceptance pending. DA-MCP-001: session-agent scope check before history/model."
},
"backend/api/v1/utils/tables_helper.py": {
"baseline_sha256": "7d02daefc0bd22fe516fdfbfd26d034c3b55c05655fcb1fd4bd9a42029d1d09b",
Expand All @@ -1121,6 +1121,18 @@
"current_sha256": "b7f39614c9c16ea17ce3418303d567ae0d3762e0b900dd1770f4c0f8c02df1ec",
"issue": "DA-013",
"reason": "Independently accepted prompt-only business template inheritance of the central model profile; template instructions and SQL/security guards preserved. Fresh runtime acceptance pending."
},
"backend/api/v1/utils/chat_helper.py": {
"baseline_sha256": "a19c1b056d58c9cf570b77eb5d6ff7e6e845a01648056636ba2b8f6f20d66385",
"current_sha256": "d545db45dc86dc5521a561abb31ea63652f100142d206184e61f59d7823fedd6",
"issue": "DA-MCP-001",
"reason": "Session continuation is scoped to the agent that created the session (cross-agent history replay closed), and saved-conversation replay additively serves the stored column-labelled result block; required by the MCP adapter. No prompt, model, SQL generation or execution change."
},
"backend/session/db_utils/session_db_utils.py": {
"baseline_sha256": "18e03d5966a451f0b1bc6d5777e2254effc1c1ad248f033d0067c7d7faf64d11",
"current_sha256": "08b3aa026c754695bf795f23191cab5f1445eebe5306cab3ced501b9b0e89499",
"issue": "DA-MCP-001",
"reason": "Session continuation is scoped to the agent that created the session (cross-agent history replay closed), and saved-conversation replay additively serves the stored column-labelled result block; required by the MCP adapter. No prompt, model, SQL generation or execution change."
}
}
}
22 changes: 22 additions & 0 deletions mcp-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# database-agent-mcp

MCP server for [FutureSmart Database Agent](https://github.com/FutureSmart-AI/database-agent).
It lets MCP-compatible applications such as Claude Code list your Database Agent
agents, ask natural-language database questions, get back the answer, generated
SQL, structured rows and chart metadata, continue conversations, and read saved
results.

It is a thin adapter over the Database Agent REST API, using an existing API
credential. The API keeps authentication, query limits, model configuration,
table visibility, SQL access controls and execution.

```sh
pip install "git+https://github.com/FutureSmart-AI/database-agent.git#subdirectory=mcp-server"
claude mcp add database-agent --scope user \
-e DATABASE_AGENT_URL=http://127.0.0.1:18000/nl2sql/api \
-e "DATABASE_AGENT_AGENT_KEYS=$AGENT_ID=$AGENT_KEY" \
-- database-agent-mcp
```

See [docs/mcp.md](../docs/mcp.md) for tools, credentials, configuration, the
Claude Code `.mcp.json` example and the equivalent direct API calls.
3 changes: 3 additions & 0 deletions mcp-server/database_agent_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""MCP server for FutureSmart Database Agent: a thin adapter over the product REST API."""

__version__ = "0.1.0"
3 changes: 3 additions & 0 deletions mcp-server/database_agent_mcp/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .server import main

main()
Loading
Loading