文件读过了,接口也接过了,这次试试数据库。
目标很具体:让 AI 查出一个演示项目里,还没关闭的两条缺陷。
我们使用 SQLite。它不需要单独安装数据库服务,Python 自带支持,适合先把流程跑通。
一、准备项目
新建文件夹 mcp-sqlite,按下面创建文件:
text
mcp-sqlite/
├── prepare.py
├── run.py
├── check.py
└── fixtures/
└── schema.sql
在项目文件夹打开终端,安装依赖。
Mac:
bash
python3 -m venv .venv
.venv/bin/python -m pip install mcp==2.3.0 pydantic==2.14.1
Windows PowerShell:
powershell
py -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install mcp==2.3.0 pydantic==2.14.1
Python 使用 3.11 或更高版本。
二、建一个演示数据库
打开 fixtures/schema.sql,复制:
sql
CREATE TABLE issues (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
project TEXT NOT NULL,
status TEXT NOT NULL CHECK(status IN ('open', 'closed'))
);
INSERT INTO issues VALUES
('DB-001', '虚构登录问题', 'demo-project', 'open');
INSERT INTO issues VALUES
('DB-002', '虚构搜索问题', 'demo-project', 'open');
INSERT INTO issues VALUES
('DB-003', '虚构已修复问题', 'demo-project', 'closed');
这里有三条问题:两条未关闭,一条已关闭。
再打开 prepare.py,复制建库代码:
python
from pathlib import Path
import sqlite3
BASE = Path(__file__).resolve().parent / "fixtures"
def prepare():
target = BASE / "demo.db"
# 已存在时直接报错,避免覆盖旧数据。
with target.open("xb"):
pass
try:
connection = sqlite3.connect(target)
try:
connection.executescript(
(BASE / "schema.sql").read_text(encoding="utf-8")
)
finally:
connection.close()
except Exception:
target.unlink(missing_ok=True)
raise
return target
if __name__ == "__main__":
print("Created", prepare().name)
Mac 执行:
bash
.venv/bin/python prepare.py
Windows 执行:
powershell
.\.venv\Scripts\python.exe prepare.py
看到下面的输出,就说明建好了:
text
Created demo.db
fixtures 文件夹中会多出一个 demo.db,它就是数据库。
建库只需要执行一次。 再次执行提示文件已存在,是为了防止覆盖数据。
三、写一个只读查询工具
打开 run.py,复制完整代码:
python
from pathlib import Path
import sqlite3
from typing import Annotated, Any, Literal
from pydantic import Field
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import ToolAnnotations
mcp = MCPServer("sqlite-reader-demo")
READ_ONLY = ToolAnnotations(
read_only_hint=True,
destructive_hint=False,
idempotent_hint=True,
open_world_hint=False,
)
DB = Path(__file__).resolve().parent / "fixtures" / "demo.db"
def connect_readonly():
connection = sqlite3.connect(
DB.resolve().as_uri() + "?mode=ro",
uri=True,
timeout=1,
)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA query_only=ON")
return connection
@mcp.tool(annotations=READ_ONLY)
def list_db_issues(
project: Literal["demo-project", "empty-project"],
status: Literal["open", "closed"] = "open",
limit: Annotated[int, Field(strict=True, ge=1, le=20)] = 10,
) -> dict[str, Any]:
"""按项目和状态查询演示缺陷,不接受 SQL,不修改数据库。"""
try:
connection = connect_readonly()
try:
rows = connection.execute(
"""
SELECT id, title, project, status
FROM issues
WHERE project = ? AND status = ?
ORDER BY id
LIMIT ?
""",
(project, status, limit),
).fetchall()
finally:
connection.close()
except sqlite3.Error:
raise ToolError(
"database_unavailable_or_invalid_schema"
) from None
return {
"source": "local-demo-sqlite",
"count": len(rows),
"issues": [dict(row) for row in rows],
}
if __name__ == "__main__":
mcp.run(transport="stdio")
这段代码限制了三件事:
- 数据库以只读方式打开。
- SQL 写在代码里,调用方只能传查询条件。
- 最多返回 20 条,避免一次取出太多数据。
SQL 中的 ? 用来绑定参数,别把查询条件直接拼进 SQL 字符串。
这里的 count 是本次返回数量,没有额外统计全部匹配记录。
四、检查能不能查到数据
打开 check.py,复制:
python
import asyncio
import json
import sys
from pathlib import Path
from mcp import Client, StdioServerParameters
async def main():
config = {
"command": sys.executable,
"args": [
str(Path(__file__).with_name("run.py").resolve())
],
"env": {
"PYTHONUTF8": "1",
"PYTHONIOENCODING": "utf-8",
},
}
async with Client(StdioServerParameters(**config)) as client:
result = await client.call_tool("list_db_issues", {
"project": "demo-project",
"status": "open",
"limit": 10,
})
if result.is_error:
for item in result.content:
print(getattr(item, "text", ""))
raise SystemExit(1)
data = result.structured_content
assert data["source"] == "local-demo-sqlite"
assert data["count"] == 2
assert [x["id"] for x in data["issues"]] == [
"DB-001", "DB-002",
]
print(json.dumps(data, ensure_ascii=False, indent=2))
empty = await client.call_tool("list_db_issues", {
"project": "empty-project",
})
assert not empty.is_error
assert empty.structured_content["count"] == 0
print("检查通过:查到两条未关闭问题,空项目返回零条。")
print("客户端配置:")
print(json.dumps(config, ensure_ascii=False, indent=2))
if __name__ == "__main__":
asyncio.run(main())
Mac 执行:
bash
.venv/bin/python check.py
Windows 执行:
powershell
.\.venv\Scripts\python.exe check.py
正常情况下,应该查到:
text
DB-001:虚构登录问题
DB-002:虚构搜索问题
最后显示:
text
检查通过:查到两条未关闭问题,空项目返回零条。
五、接进 AI 客户端
按照第 04 篇的方法,添加本地 stdio 服务。
名称可以填 sqlite-issues,其他字段使用 check.py 输出的配置。
给当前 Agent 启用服务,发送:
text
请调用 list_db_issues。
project 填 demo-project
status 填 open
limit 填 10
列出本次查到的问题编号和标题。
只根据工具结果回答。
确认调用记录中出现 list_db_issues,返回两条未关闭问题。
再把 status 改成 closed,应该查到:
text
DB-003:虚构已修复问题
这样就能确认,查询条件确实生效了。
六、别忽略这两个区别
查不到数据,不等于数据库出错。
查询 empty-project,正常结果是零条;数据库不存在或表结构不对,则应该报错。
允许查询,不等于允许执行任意 SQL。
这篇只开放了"按项目和状态查问题"的入口,没有开放改表、删库或自定义 SQL 的能力。
如果修改了 schema.sql,已有的 demo.db 不会自动更新。需要重建时,先确认它确实是本篇生成的演示库,再处理旧文件,别误删自己的数据库。
本文已在 Mac 上完成建库和 MCP stdio 查询验证。Windows、真实业务数据库和模型调用未实测。