内容
# SmartGIL - 与数据库进行自然语言对话
> 实践工作坊:构建一个与 PostgreSQL 数据库集成的 MCP 服务器
[](https://nodejs.org)
[](https://www.typescriptlang.org)
[](https://www.postgresql.org)
[](https://podman.io)
[](https://aistudio.google.com)
[](https://modelcontextprotocol.io)
在这个工作坊中,您将从零开始构建一个 **MCP(Model Context Protocol)** 服务器,允许像 Claude 这样的语言模型直接回答关于 PostgreSQL 数据库的问题。您无需手动编写 SQL 查询,只需用中文提问,系统便会找到正确的表格,生成查询语句并返回答案。
**示例:**
> _"哪些教授在星期四下午有空?"_
>
> → SmartGIL 找到正确的表格,生成正确的 SQL 查询并以自然语言回答。
---
## 使用的技术
项目中使用的语言、框架和工具。
| 技术 | 在项目中的角色 |
| --- | --- |
| **TypeScript** | MCP 服务器的主要语言 |
| **Express.js** | 通过 SSE 公开 MCP 协议的 HTTP 服务器 |
| **PostgreSQL** | 存储数据的关系型数据库 |
| **ChromaDB** | 向量数据库 - 存储索引以进行语义搜索 |
| **Google Gemini API** | 通过 `gemini-embedding-2` 生成模式嵌入(免费,无需安装) |
| **Podman** | 容器运行时(替代 Docker,无需 root 守护进程) |
| **@modelcontextprotocol/sdk** | Anthropic 官方 MCP SDK |
| **Zod** | 执行时模式验证和类型定义 |
---
## 应用场景
该项目可应用于多种现实场景:
- **内部数据分析** - 对企业数据库进行自然语言查询
- **支持和帮助台** - 让支持人员无需了解 SQL 即可查询数据库
- **数据探索** - 科学家和分析师无需预先文档即可发现数据库中有哪些数据
- **与 AI 自动化** - 智能体自主搜索、分析和报告数据
- **产品原型设计** - 几分钟内将 LLMs 与任何现有的 PostgreSQL 数据库集成
---
## 目录
- [SmartGIL - 与数据库进行自然语言对话](#smartgil---与数据库进行自然语言对话)
- [使用技术](#使用技术)
- [应用场景](#应用场景)
- [目录](#目录)
- [安装](#安装)
- [Podman(容器运行时)](#podman容器运行时)
- [Google Gemini API(嵌入式)](#google-gemini-api嵌入式)
- [Node.js 20+](#nodejs-20)
- [Claude Desktop(可选,推荐)](#claude-desktop可选推荐)
- [其他资源](#其他资源)
- [为什么 IA + 数据库?](#为什么-ia-数据库)
- [什么是 MCP?](#什么是-mcp)
- [什么是关系型数据库?](#什么是关系型数据库)
- [什么是嵌入式和向量搜索?](#什么是嵌入式和向量搜索)
- [什么是 AI API?](#什么是-ai-api)
- [我们将构建什么](#我们将构建什么)
- [MCP 服务器公开的工具](#mcp-服务器公开的工具)
- [完整问题流程](#完整问题流程)
- [路线图](#路线图)
- [第 1 阶段 - 环境配置](#第-1-阶段-环境配置)
- [1.1 - 克隆项目](#11-克隆项目)
- [1.2 - 配置环境变量](#12-配置环境变量)
- [1.3 - 验证环境](#13-验证环境)
- [1.4 - 启动服务](#14-启动服务)
- [1.5 - 验证一切正常运行](#15-验证一切正常运行)
- [第 2 阶段 - 探索项目结构](#第-2-阶段-探索项目结构)
- [第 3 阶段 - 数据库](#第-3-阶段-数据库)
- [3.1 - 学术演示模式](#31-学术演示模式)
- [3.2 - 通过 `information_schema` 读取模式](#32-通过-information_schema读取模式)
- [第 4 阶段 - ChromaDB 和向量嵌入](#第-4-阶段-chromadb和向量嵌入)
- [4.1 - ChromaDB 客户端(`chroma-client.ts`)](#41-chromadb客户端chroma-clientts)
- [4.2 - 使用 Gemini API 生成嵌入式](#42-使用-gemini-api生成嵌入式)
- [4.3 - 使用 HTTP 客户端测试 API](#43-使用-http客户端测试-api)
- [4.4 - 在 ChromaDB 中索引模式](#44-在-chromadb中索引模式)
- [4.5 - 搜索相关表格](#45-搜索相关表格)
- [第 5 阶段 - MCP 服务器](#第-5-阶段-mcp服务器)
- [5.1 - 管理 PostgreSQL 连接(`db-manager.ts`)](#51-管理-postgresql连接db-managerts)
- [5.2 - 带有 SSE 的 Express 服务器(`index.ts`)](#52-带有-sse的-express服务器indexts)
- [5.3 - 注册工具(`server.ts`)](#53-注册工具serverts)
- [第 6 阶段 - MCP 工具](#第-6-阶段-mcp工具)
- [工具 1 - `conectar_banco`](#工具-1---conectar_banco)
- [工具 2 - `buscar_informacoes`](#工具-2---buscar_informacoes)
- [工具 3 - `executar_query`](#工具-3---executar_query)
- [工具 4 - `listar_schema`](#工具-4---listar_schema)
- [第 7 阶段 - 使用 Podman 的容器](#第-7-阶段-使用-podman的容器)
- [7.1 - Dockerfile](#71-dockerfile)
- [7.2 - `compose.yml`](#72-composeyml)
- [7.3 - 基本命令](#73-基本命令)
- [第 8 阶段 - 集成和测试](#第-8-阶段-集成和测试)
- [8.1 - 健康检查](#81-健康检查)
- [8.2 - 配置 Claude Desktop](#82-配置-claude-desktop)
- [8.3 - 完整的会话](#83-完整的会话)
- [替代方案:Ollama(本地 LLMs)](#替代方案ollama本地-llms)
- [最佳实践](#最佳实践)
- [作者](#作者)
- [许可证](#许可证)
---
## 安装
请准确遵循环境配置指南,以确保项目在工作坊期间正确运行。
### Podman(容器运行时)
Podman 6.0+ 是必需的 - `podman compose` 命令从此版本开始捆绑。
| 系统 | 命令 / 链接 |
| --- | --- |
| **Ubuntu/Debian** | `sudo apt install podman` |
| **Fedora/RHEL** | `sudo dnf install podman` |
| **macOS** | `brew install podman` → `podman machine init` → `podman machine start` |
| **Windows** | [Podman Desktop](https://podman-desktop.io) |
验证安装:
```bash
podman --version # 必须为 6.0+
podman compose version
```
### Google Gemini API(嵌入式)
SmartGIL 使用 Google Gemini API 生成数据库模式的嵌入式。该 API 免费,不需要信用卡,配置时间不到 2 分钟。
**步骤 1 - 访问 Google AI Studio:**
打开 [aistudio.google.com](https://aistudio.google.com) 并使用 Google 帐户登录。
**步骤 2 - 创建 API 密钥:**
1. 点击侧边栏中的 **"获取 API 密钥"**
2. 点击 **"创建 API 密钥"**
3. 选择现有的 Google Cloud 项目(或创建一个新项目 - 免费)
4. 复制生成的密钥(以 `AQ.` 开头 - 这是 Google AI Studio 当前的格式)
**步骤 3 - 将密钥添加到项目:**
```bash
# 复制示例文件
cp .env.example .env
# 编辑 .env 并用实际密钥替换 "sua_chave_aqui"
# GEMINI_API_KEY=AIzaSy...
```
> **Gemini 免费层:** 每天 1,500 次请求,每分钟 100 次 - 足够用于此工作坊和小型实际项目。
### Node.js 20+
仅客户端 CLI(`ollama-client`)需要。
在 [nodejs.org](https://nodejs.org) 下载或使用 `nvm` 等版本管理器:
```bash
nvm install 20
nvm use 20
node --version # 必须为 v20+
```
### Claude Desktop(可选,推荐)
用于与图形界面聊天体验。 在 [claude.ai/download](https://claude.ai/download) 下载。
### 其他资源
- **[DBeaver](https://dbeaver.io)** - 用于可视化探索数据库的 SQL 客户端(可选)
- **[Reqly](https://github.com/reqly-labs/reqly)** 或 **[Insomnia](https://insomnia.rest)** - 用于测试服务器和 Gemini API 的 HTTP 端点(可选,但非常有用)
---
## 为什么 IA + 数据库?
想象您是大学的分析师,每天需要回答有关学术数据的提问。对于每个问题,您需要:
1. **了解结构** - 知道存在 `turmas`、`professores`、`horarios` 表
2. **编写 SQL** - 带有正确的 JOIN、过滤器和聚合
3. **解释结果** - 将行转换为有用信息
使用 MCP + LLM,您可以直接用中文提问。系统会完成所有其他工作。
### 🔌 什么是 MCP?
**MCP(Model Context Protocol)** 是由 Anthropic 创建的开放协议,用于规范语言模型如何与工具和外部数据源通信。
在 MCP 之前,每个集成都是专有的和不兼容的。MCP 提供了一个共同的契约:
```
┌─────────────────────────────┐
│ MCP 客户端 │
│ (Claude Desktop 等) │
└────────────┬────────────────┘
│ 标准协议(SSE/HTTP)
┌────────────▼────────────────┐
│ MCP 服务器 │ ← 我们将构建
│ (我们的 mcp-server) │
└────────────┬────────────────┘
│
┌──────▼──────┐
│ 工具 │
│ (数据库、API)│
└─────────────┘
```
MCP 服务器公开 **工具** - 具有名称和参数的函数,LLM 可以调用。当您提出问题时,LLM 会自动决定调用哪个工具以及使用哪些参数。
### 🗄️ 什么是关系型数据库?
关系型数据库将信息组织成 **表格** - 具有行(记录)和列(属性)的结构。表格通过键相互关联:
```
professores
┌────┬──────────────┬─────────────────┐
│ id │ 名称 │ 部门 ID │
├────┼──────────────┼─────────────────┤
│ 1 │ Ana Lima │ 1 │──┐
│ 2 │ Bruno Costa │ 1 │ │
└────┴──────────────┴─────────────────┘ │
│ 外键(FK)
departamentos │
┌────┬──────────────────┐ │
│ id │ 名称 │ │
├────┼──────────────────┤ │
│ 1 │ 计算机科学 │◄───────────────┘
│ 2 │ 数学 │
└────┴──────────────────┘
```
**模式** 是此结构的完整描述 - 有哪些表,每个表有哪些列,它们是什么类型以及它们如何关联。SmartGIL 读取此模式并使其可由 LLM 搜索。
**SQL(Structured Query Language)** 是查询关系数据库的语言:
```sql
-- "哪些教授属于计算机科学系?"
SELECT p.nome
FROM professores p
JOIN departamentos d ON d.id = p.departamento_id
WHERE d.nome = '计算科学与数学';
```
### 🔍 什么是嵌入式和向量搜索?
**嵌入式** 是文本的数值表示 - 一个带有数百个数字的向量。具有相似含义的文本在向量空间中接近:
```
"星期四有空的教授" → [0.23, -0.41, 0.87, 0.12, ...]
"星期四有空的教师" → [0.21, -0.39, 0.85, 0.14, ...] ← 接近!
"课程注册人数" → [-0.12, 0.67, -0.23, 0.55, ...] ← 远离
```
**ChromaDB** 是一个向量数据库 - 存储每个表的模式嵌入式,并允许找到哪些表与问题相关,即使单词不相同。
```
问题: "星期四有哪些教授有空?"
↓ 生成问题嵌入式(通过 Gemini API)
ChromaDB 与表格嵌入式比较
↓ 找到最接近的
返回: "教授" 和 "教授可用性" 表格
```
### 🤖 什么是 AI API?
**AI API(Application Programming Interface)** 是云服务通过 HTTP 调用公开的 AI 功能 - 无需安装 GPU,无需管理模型。
您通过 HTTP 发送文本并收到由语言模型在专用服务器上处理的结果。
```
您的服务器 互联网 Google 服务器
┌──────────┐ HTTP POST ┌─────────────────────────────┐
│ SmartGIL │ ──────────► │ Gemini API │
│ │ │ 模型:gemini-embedding-2 │
│ │ ◄────────── │ → 返回 [3072 个数字] │
└──────────┘ JSON 响应 └─────────────────────────────┘
```
**为什么使用云 AI API 而不是本地运行?
| 方面 | 云 API(Gemini) | 本地(Ollama) |
| -------------------- | ------------------------------------------ | ------------------------------------- |
| **安装** | 无需安装 - 只需 API 密钥 | ~274 MB 下载模型 |
| **硬件** | 任何计算机 | 需要足够的 RAM/GPU |
| **速度** | 非常快(Google 使用 TPU) | 取决于本地硬件 |
| **成本** | 每天免费 1,500 次请求 | 安装后免费 |
| **隐私** | 数据通过 Google | 本地存储,无外部数据 |
| **离线** | 需要互联网 | 无需互联网 >
> **对于此工作坊**,我们使用 Gemini API,因为它更简单并且在任何环境下都有效
>
> **Ollama 也是一个优秀的选项**,对于需要完全隐私或离线工作的人。见章节 [替代方案:Ollama(本地 LLMs)](#-替代方案ollama本地-llms)。---
## 我们将构建什么
```
┌────────────────────────────────────────────────────────────────┐
│ 您的机器 │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Claude Desktop │ │ ollama-client(CLI) │ │
│ | │
│ │ (接口 MCP) │ │ ollama-client (Node.js + Ollama) │ │
│ └────────┬─────────┘ └──────────┬───────────────┘ │
│ │ │ MCP/SSE │
└───────────┼───────────────────────────────┼───────────────────┘
│ HTTP port 3001 │
┌───────────▼───────────────────────────────▼───────────────────┐
│ Podman Compose │
│ │
│ ┌─────────────────────────────┐ ┌────────────────────────┐ │
│ │ mcp-server (port 3001) │ │ ChromaDB (port 8000) │ │
│ │ Express + SSE │◄──│ 模式索引 │ │
│ │ TypeScript │ │ 向量搜索 │
│ └──────────────┬──────────────┘ └────────────────────────┘ │
│ │ pg(node-postgres) ↑ 嵌入式 │
└─────────────────┼─────────────────────────────────────────────┘
│ TCP/IP ┌──────────────────────┐
┌─────────▼────────────┐ │ Google Gemini API │
│ PostgreSQL │ │ gemini-embedding-2 │
│ postgres-demo:5432 │ │ (云 - 免费) │
│ (数据库) │ └──────────────────────┘
└──────────────────────┘
```
### MCP 服务器公开的工具
| 工具 | 功能 |
| --- | --- |
| `conectar_banco` | 连接到 PostgreSQL,读取整个模式并将其编入 ChromaDB |
| `buscar_informacoes` | 语义搜索相关表格 |
| `executar_query` | 在数据库上执行 SQL 并格式化结果 |
| `listar_schema` | 显示完整的模式(表格、列、外键)以进行验证 |
---
## 路線图
### 第 1 阶段 - 环境配置
#### 1.1 - 克隆项目
```bash
git clone https://github.com/your-repo/smartgil.git
```
#### 1.2 - 配置环境变量
```bash
cp .env.example .env
```
编辑 `.env` 并用实际密钥替换 `sua_chave_aqui`。
#### | 1.3 - 验证环境
```bash
podman --version # 必须是 v20+
podman compose version
```
#### 1.4 - 启动服务
```bash
podman compose up
```
#### 1.5 - 验证一切正常运行
```bash
curl http://localhost:3001/health
```
### 第 2 阶段 - 探索项目结构
### 第 3 阶段 - 数据库
#### 3.1 - 学术演示模式
### 第 4 阶段 - ChromaDB 和向量嵌入
#### 4.1 - ChromaDB 客户端(`chroma-client.ts`)
### 第 4 阶段 - ChromaDB 和向量嵌入
#### 4.2 - 使用 Gemini API 生成嵌入式
### 第 4 阶段 - ChromaDB 和向量嵌入
#### 4.3 - 使用 HTTP 客户端测试 API
### 第 4 阶段 - ChromaDB 和向量嵌入
#### | 4.4 - 在 ChromaDB 中索引模式
### 第 4 阶段 - ChromaDB 和向量嵌入
#### 4.5 - 搜索相关表格
### 第 5 阶段 - MCP 服务器
#### 5.1 - 管理 PostgreSQL 连接(`db-manager.ts`)
### 第 5 阶段 - MCP 服务器
#### 5.2 - 带有 SSE 的 Express 服务器(`index.ts`)
### 第 5 阶段 - MCP 服务器
#### 5.3 - 注册工具(`server.ts`)
### 第 6 阶段 - MCP 工具
#### 工具 1 - `conectar_banco`
### 第 6 阶段 - MCP 工具
#### 工具 2 - `buscar_informacoes`
### 第 6 阶段 - MCP 工具
#### 工具 3 - `executar_query`
### 第 6 阶段 - MCP 工具
#### 工具 4 - `listar_schema`
### 第 7 阶段 - 使用 Podman 的容器
#### 7.1 - Dockerfile
### 第 7 阶段 - 使用 Podman 的容器
#### 7.2 - `compose.yml`
### 第 7 阶段 - 使用 Podman 的容器
#### 7.3 - 基本命令
### 第 8 阶段 - 集成和测试
#### 8.1 - 健康检查
#### 8.2 - 配置 Claude Desktop
#### 8.3 - 完整的会话
## 替代方案:Ollama(本地 LLMs)
## 最佳实践
## 作者
## 许可证
### 完整问题流程
```
1. 你: "哪些教授在星期四下午有空?"
2. LLM 调用: buscar_informacoes("教授 星期四 下午")
→ API Gemini 生成查询的 embedding
→ ChromaDB 返回: 可用性_教授, 教授
3. LLM 调用: executar_query("SELECT p.nome FROM 教授 p
JOIN 可用性_教授 d ON d.教授_id = p.id
WHERE d.星期几 = '星期四' AND d.可用 = true
AND d.开始时间 <= '14:00' AND d.结束时间 >= '18:00'")
4. 结果 → LLM 用自然语言回答
```
---
## 路线图
### ⚙️ 阶段 1 - 环境配置
在编写任何代码之前,我们需要确保所有工具都已安装并正确运行。
#### 1.1 - 克隆项目
```bash
git clone https://github.com/claudiogpt/SmartGIU.git SmartGIL
cd SmartGIL
```
#### 1.2 - 配置环境变量
```bash
cp .env.example .env
```
打开 `.env` 并将 `sua_chave_aqui` 替换为你的 Gemini API 密钥:
```env
# Google Gemini API 密钥 - 在 https://aistudio.google.com 获取
GEMINI_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# 远程数据库的 SSL(本地演示数据库为 false)
DB_SSL=false
```
> **安全:** 永远不要公开分享你的 API 密钥。`.gitignore` 已经忽略了 `.env` 文件 - 它不会进入仓库。
#### 1.3 - 检查环境
```bash
bash scripts/setup-podman.sh
```
脚本检查 Podman、Podman Compose 和 `.env` 中是否存在 `GEMINI_API_KEY`。在继续之前,请修正任何标记为 `[XX]` 的项目。
#### 1.4 - 启动服务
```bash
# 主要服务 + 演示数据库
podman compose --profile demo up -d
```
在第一次执行时,Podman 会下载镜像并编译服务器 - 请等待约 2 分钟。
#### 1.5 - 检查所有服务是否正在运行
```bash
# MCP 服务器健康检查
curl http://localhost:3001/health
# 预期:{"status":"ok","service":"smartgiu-mcp-server"}
# ChromaDB 健康检查
curl http://localhost:8000/api/v1/heartbeat
# 预期:{"nanosecond heartbeat":...}
# 所有容器的状态
podman compose ps
```
---
### 🏗️ 阶段 2 - 探索项目结构
在修改任何文件之前,请了解已存在的内容和原因。
```
SmartGIL/
├── compose.yml ← 编排 3 个容器(chromadb, mcp-server, postgres-demo)
├── .env / .env.example ← 环境变量(GEMINI_API_KEY, DB_SSL)
├── scripts/
│ └── setup-podman.sh ← 环境检查脚本
│
├── mcp-server/ ← 项目核心(TypeScript)
│ ├── Dockerfile ← 如何构建服务器镜像
│ ├── package.json ← 依赖项:express, @mcp/sdk, pg, zod
│ ├── tsconfig.json ← TypeScript 配置为 NodeNext 模块
│ └── src/
│ ├── index.ts ← Express 服务器 + SSE/messages/health 端点
│ ├── server.ts ← McpServer 实例 + 4 个工具的注册
│ ├── db-manager.ts ← 单例 pg.Pool - 每个进程 1 个活动连接
│ ├── chroma-client.ts ← ChromaDB REST 客户端 + Gemini embedding 调用
│ └── tools/
│ ├── connect-database.ts ← 工具 1:连接 + 索引模式
│ ├── search-information.ts ← 工具 2:语义搜索 ChromaDB
│ ├── execute-query.ts ← 工具 3:在数据库中执行 SQL
│ └── list-schema.ts ← 工具 4:完整的模式转储
│
├── ollama-client/ ← Ollama 聊天客户端(使用 Ollama 作为 LLM - 可选)
│ └── src/index.ts
│
└── demo-db/
└── init.sql ← 具有 6 个表和示例数据的学术模式
```
**`mcp-server/package.json` 的主要依赖项:**
```json
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.x", // MCP 协议
"express": "^4.x", // HTTP 服务器
"pg": "^8.x", // PostgreSQL 驱动程序
"zod": "^3.x", // 模式验证
"cors": "^2.x" // Claude Desktop 的 CORS
},
"devDependencies": {
"tsx": "^4.x", // 直接执行 TypeScript
"typescript": "^5.x"
}
}
```
> **注意:** Gemini API 通过 Node.js 原生 `fetch` 访问 - 无需额外的 npm 包。
> **关于 `.js` 扩展名的说明:** 项目在 `tsconfig.json` 中使用 `"module": "NodeNext"`。这要求内部导入使用 `.js` 扩展名,即使是 `.ts` 文件:
>
> ```typescript
> import { getPool } from "./db-manager.js"; // 正确 ✅
> import { getPool } from "./db-manager"; // 错误 ❌
> ```
---
### 📊 阶段 3 - 数据库
#### 3.1 - 演示学术模式
该项目包含一个示例 PostgreSQL 数据库,模拟大学管理系统。参见 `demo-db/init.sql`:
```sql
-- 大学部门
CREATE TABLE 部门 (
id SERIAL PRIMARY KEY,
名称 VARCHAR(100) NOT NULL,
代码 VARCHAR(10) UNIQUE NOT NULL
);
-- 绑定到部门的教授
CREATE TABLE 教授 (
id SERIAL PRIMARY KEY,
名称 VARCHAR(100) NOT NULL,
电子邮件 VARCHAR(100) UNIQUE NOT NULL,
部门_id INTEGER REFERENCES 部门(id),
制度 VARCHAR(20) NOT NULL DEFAULT '40h' -- 20h | 40h | DE
);
-- 部门提供的课程
CREATE TABLE 课程 (
id SERIAL PRIMARY KEY,
名称 VARCHAR(100) NOT NULL,
代码 VARCHAR(20) UNIQUE NOT NULL,
学时 INTEGER NOT NULL, -- 小时/周
部门_id INTEGER REFERENCES 部门(id)
);
-- 每个学科在学期中的班级
CREATE TABLE 班级 (
id SERIAL PRIMARY KEY,
学科_id INTEGER NOT NULL REFERENCES 学科(id),
教授_id INTEGER REFERENCES 教授(id), -- 可以为 NULL
学期 VARCHAR(10) NOT NULL, -- 例如:2025.1
班级代码 VARCHAR(10) NOT NULL,
名额 INTEGER DEFAULT 40,
已注册学生 INTEGER DEFAULT 0
);
-- 课程时间表(星期几 + 教室 + 时间)
CREATE TABLE 时间表 (
id SERIAL PRIMARY KEY,
班级_id INTEGER NOT NULL REFERENCES 班级(id),
星期几 VARCHAR(15) NOT NULL, -- 星期一 | 星期二 | 星期三 | 星期四 | 星期五
开始时间 TIME NOT NULL,
结束时间 TIME NOT NULL,
教室 VARCHAR(20)
);
-- 教授声明的可用性
CREATE TABLE 教授可用性 (
id SERIAL PRIMARY KEY,
教授_id INTEGER NOT NULL REFERENCES 教授(id),
星期几 VARCHAR(15) NOT NULL,
开始时间 TIME NOT NULL,
结束时间 TIME NOT NULL,
可用 BOOLEAN NOT NULL DEFAULT TRUE,
备注 VARCHAR(200)
);
```
数据库已填充:
- **3 个部门** (计算机、人类学、工程)
- **10 名教授** 在不同制度下
- **8 门课程** 带有学时
- **15 个班级** 在 2025.1 学期
- **30+ 条记录** 的时间表和可用性
#### 3.2 - 通过 `information_schema` 读取模式
PostgreSQL 通过名为 `information_schema` 的特殊模式公开任何数据库的结构。`connect-database` 就是这样自动发现所有表的:
```sql
-- 列出模式中的所有用户表
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
ORDER BY table_name;
-- 列出所有列及其类型
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position;
-- 列出所有外键关系
SELECT
kcu.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema = 'public';
```
这三个查询一起为系统提供了描述任何 PostgreSQL 数据库所需的一切。
---
### 🔍 阶段 4 - ChromaDB 和向量嵌入
#### 4.1 - ChromaDB 客户端(`chroma-client.ts`)
`mcp-server/src/chroma-client.ts` 文件实现了与 ChromaDB 和 Gemini API 的所有通信 - 只使用原生 `fetch`,无需额外包:
```typescript
// 通过环境变量配置
const CHROMA_URL = process.env.CHROMDB_URL ?? "http://chromadb:8000";
const GEMINI_API_KEY = process.env.GEMINI_API_KEY ?? "";
const EMBED_MODEL = "gemini-embedding-2";
const COLLECTION_NAME = "db_schema";
const GEMINI_EMBED_URL = `https://generativelanguage.googleapis.com/v1/models/${EMBED_MODEL}:embedContent`;
```
#### 4.2 - 使用 Gemini API 生成嵌入
为了将文本转换为数值向量,我们向 Gemini API 发出 HTTP 请求。`embedContent` 端点一次处理一个文本 - 为了索引多个表,我们使用 `Promise.all` 并行调用:
```typescript
async function getEmbedding(text: string): Promise<number[]> {
const res = await fetch(GEMINI_EMBED_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-goog-api-key": GEMINI_API_KEY,
},
body: JSON.stringify({
model: "models/gemini-embedding-2",
content: { parts: [{ text }] },
}),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Gemini embedding error (${res.status}): ${body}`);
}
const data = await res.json();
return data.embedding.values;
// 每个向量有 3072 个维度
}
async function getEmbeddingsBatch(texts: string[]): Promise<number[][]> {
// 为每个文本并行调用 embedContent
return Promise.all(texts.map((text) => getEmbedding(text)));
}
```
**`embedContent` 请求格式:**
```
POST https://generativelanguage.googleapis.com/v1/models/gemini-embedding-2:embedContent
x-goog-api-key: YOUR_KEY
Content-Type: application/json
{
"model": "models/gemini-embedding-2",
"content": { "parts": [{ "text": "可用性_教授 表..." }] }
}
```
**响应:**
```json
{
"embedding": {
"values": [0.021, -0.043, 0.187, ...]
}
}
```
#### 4.3 - 使用 HTTP 客户端测试 API
在集成到代码之前,直接使用 HTTP 客户端测试 Gemini API 非常有用。这确认了您的密钥有效,并帮助您理解响应格式。
**选项 A - curl(终端):**
```bash
# 将 YOUR_KEY 替换为您的 GEMINI_API_KEY(AQ... 格式)
curl -s \
"https://generativelanguage.googleapis.com/v1/models/gemini-embedding-2:embedContent" \
-H "Content-Type: application/json" \
-H "x-goog-api-key: YOUR_KEY" \
-d '{
"model": "models/gemini-embedding-2",
"content": {
"parts": [{"text": "星期四可用教授"}]
}
}'
```
结果应为包含 `embedding.values` 字段的 JSON,包含 3072 个数字。
**选项 B - Reqly(带有视觉界面的 HTTP 客户端):**
[Reqly](https://github.com/reqly-labs/reqly) 是一个现代且免费的 HTTP 客户端。按如下方式配置:
1. **方法:** `POST`
2. **网址:** `https://generativelanguage.googleapis.com/v1/models/gemini-embedding-2:embedContent`
3. **头部:**
- `Content-Type: application/json`
- `x-goog-api-key: YOUR_KEY`
4. **正文(JSON):**
```json
{
"model": "models/gemini-embedding-2",
"content": {
"parts": [{ "text": "星期四可用教授" }]
}
}
```
5. 点击 **发送** 并观察包含 `embedding.values` 的响应。
> **提示:** 如果收到 404 错误,请检查模型名称 (`gemini-embedding-2`) 并确保密钥位于 `x-goog-api-key` 头部,而不是 URL 中。
#### 4.4 - 在 ChromaDB 中索引模式
当调用 `connect-database` 时,每个表都会变成一个文本文档,转换为嵌入并存储:
```typescript
export async function indexDocuments(
docs: Array<{ id: string; text: string; metadata: Record<string, string> }>,
): Promise<void> {
const collection = await getOrCreateCollection();
// 一次发送所有文档到 Gemini(每批最多 100 个)
const embeddings = await getEmbeddingsBatch(docs.map((d) => d.text));
// 在 ChromaDB 中存储文档 + 嵌入
await fetch(`${CHROMA_URL}/api/v1/collections/${collection.id}/upsert`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ids: docs.map((d) => d.id),
embeddings,
documents: docs.map((d) => d.text),
metadatas: docs.map((d) => d.metadata),
}),
});
}
```
`connect-database.ts` 中按以下方式生成每个表的文本:
```
表:教授可用性
列:
id:integer(必填)
教授_id:integer(必填)
星期几:character varying(必填)
开始时间:time without time zone(必填)
结束时间:time without time zone(必填)
可用:boolean(必填)
备注:character varying
关系(FK):
教授_id → 教授.id
```
#### 4.5 - 搜索相关表
当 `search-information` 被调用带有查询时,会发生与查询文本相同的过程:
```typescript
export async function search(
queryText: string,
nResults = 5,
): Promise<Array<{ text: string; metadata: Record<string, string> }>> {
const collection = await getOrCreateCollection();
// 1. 通过 Gemini API 将查询转换为嵌入
const [queryEmbedding] = await getEmbeddingsBatch([queryText]);
// 2. 在向量空间中搜索最接近的文档
const res = await fetch(
`${CHROMA_URL}/api/v1/collections/${collection.id}/query`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query_embeddings: [queryEmbedding],
n_results: Math.min(nResults, 10),
include: ["documents", "metadatas", "distances"],
}),
},
);
const data = await res.json();
// 3. 按语义相关性返回排序文档
return data.documents[0]
.map((text: string | null, i: number) => ({
text: text ?? "",
metadata: data.metadatas[0][i] ?? {},
}))
.filter((r: { text: string }) => r.text !== "");
}
```
---
### ⚙️ 阶段 5 - MCP 服务器
#### 5.1 - 管理 PostgreSQL 连接(`db-manager.ts`)
`db-manager.ts` 维护一个具有活动连接的单例:
```typescript
import pg from "pg";
const { Pool } = pg;
let pool: pg.Pool | null = null;
let connectionInfo: ConnectionConfig | null = null;
export async function connectDb(config: ConnectionConfig): Promise<void> {
// 如果存在,则关闭之前的连接
if (pool) {
await pool.end().catch(() => {});
pool = null;
}
const sslConfig =
process.env.DB_SSL === "false" ? false : { rejectUnauthorized: false };
pool = new Pool({
host: config.host,
port: config.port,
database: config.database,
user: config.user,
password: config.password,
ssl: sslConfig,
connectionTimeoutMillis: 10_000,
idleTimeoutMillis: 30_000,
max: 5, // 池中同时连接的最大数量
});
// 立即测试连接
const client = await pool.connect();
try {
await client.query("SELECT 1");
} finally {
client.release();
}
connectionInfo = config;
}
export function isConnected(): boolean {
return pool !== null;
}
export function getPool(): pg.Pool {
if (!pool)
throw new Error("数据库未连接。先调用 connect-database。");
return pool;
}
```
> **设计决策:** 凭证仅在容器运行时保存在内存中。每次重启都需要调用 `connect-database`。这是有意的 - 避免将密码保存在磁盘上。
#### 5.2 - 带有 SSE 的 Express 服务器(`index.ts`)
`index.ts` 配置了使用 MCP 的 HTTP 服务器:
```typescript
import express from "express";
import cors from "cors";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { createMcpServer } from "./server.js";
const app = express();
app.use(cors());
app.use(express.json());
const { server, transports } = createMcpServer();
// 健康检查 - 确认服务器正在运行
app.get("/health", (_req, res) => {
res.json({ status: "ok", service: "smartgiu-mcp-server" });
});
// SSE 端点 - 每个 MCP 客户端都连接到此处并保持流打开
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
transports.set(transport.sessionId, transport);
res.on("close", () => {
transports.delete(transport.sessionId); // 当客户端断开时清除
});
await server.connect(transport);
});
// 消息端点 - 客户端在此处发送工具调用
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
return res
.status(404)
.json({ error: `未找到会话:${sessionId}` });
}
await transport.handlePostMessage(req, res, req.body);
});
app.listen(3001, () => {
console.log("SmartGIL MCP 服务器运行在端口 3001 上");
});
```
**为什么使用 SSE(服务器发送事件)?** Claude Desktop 使用 SSE 作为 MCP 传输 - 客户端打开一个持久的 HTTP 连接,服务器将事件发送回去。它很简单,适用于任何 HTTP,不需要 WebSockets。
#### 5.3 - 注册工具(`server.ts`)
`server.ts` 创建MCP实例并注册四个工具及其Zod模式:
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// 导入每个工具的处理器...
// 创建MCP服务器
export function createMcpServer() {
const server = new McpServer({ name: "smartgil", version: "1.0.0" });
const transports = new Map<string, SSEServerTransport>();
// 注册工具
server.tool(
"conectar_banco",
"连接到PostgreSQL并将模式索引到ChromaDB以进行语义搜索。",
{
host: z.string().describe("主机名或IP地址"),
port: z.number().int().default(5432).describe("TCP端口(默认:5432)"),
database: z.string().describe("数据库名"),
usuario: z.string().describe("PostgreSQL用户名"),
senha: z.string().describe("用户密码"),
},
conectarBanco,
);
server.tool(
"buscar_informacoes",
"语义搜索相关表格以回答问题。",
{
consulta: z.string().describe("自然语言问题"),
n_resultados: z.number().int().min(1).max(10).default(5),
},
buscarInformacoes,
);
server.tool("executar_query", "...", { sql: z.string() }, executarQuery);
server.tool("listar_schema", "...", {}, listarSchema);
return { server, transports };
}
```
第二个`server.tool()`的参数是**描述** - 这是LLM读取以决定调用哪个工具的内容。良好的描述至关重要。
---
### 🔧 第 6 步 - MCP 工具
每个工具都是`src/tools/`下的独立文件。它们都遵循相同的返回值约定: `{ content: [{ type: 'text', text: string }] }`.
#### 工具 1 - `conectar_banco`
连接到数据库,通过`information_schema`读取模式并将其索引到ChromaDB:
```typescript
export async function conectarBanco(input: {
host: string; port: number; database: string; usuario: string; senha: string;
}) {
try {
// 1. 连接到PostgreSQL
await connectDb({ host: input.host, port: input.port,
database: input.database, user: input.usuario,
password: input.senha });
// 2. 并行读取表、列和外键
const [tablesRes, columnsRes, fkRes] = await Promise.all([
query(`SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`),
query(`SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns WHERE table_schema = 'public'
ORDER BY table_name, ordinal_position`),
query(`SELECT kcu.table_name, kcu.column_name,
ccu.table_name AS foreign_table_name, ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`),
]);
// 3. 构建文档并索引到ChromaDB(通过Gemini API嵌入)
await resetCollection();
const docs = tablesRes.rows.map(t => ({
id: `table_${t.table_name}`,
text: formatTableDescription(t.table_name, columnsRes.rows, fkRes.rows),
metadata: { table_name: t.table_name, ... },
}));
await indexDocuments(docs);
// 4. 为LLM返回摘要
return { content: [{ type: 'text' as const, text: `✅ 已连接。${docs.length}个表已索引。` }] };
} catch (err) {
return { content: [{ type: 'text' as const, text: `❌ 错误:${err}` }], isError: true };
}
}
```
#### 工具 2 - `buscar_informacoes`
在ChromaDB中进行向量搜索并获取问题文本:
```typescript
export async function buscarInformacoes(input: {
consulta: string;
n_resultados: number;
}) {
if (!isConnected()) {
return {
content: [
{ type: "text" as const, text: "⚠️ 请先使用conectar_banco。"},
],
isError: true,
};
}
const results = await search(input.consulta, input.n_resultados);
const formatted = results
.map(
(r, i) =>
`### 结果 ${i + 1} - ${r.metadata.table_name}\n\n${r.text}`,
)
.join("\n\n---\n\n");
return {
content: [
{ type: "text" as const, text: `🔍 相关表格:\n\n${formatted}` },
],
};
}
```
#### 工具 3 - `executar_query`
执行LLM生成的SQL并格式化结果为Markdown表格:
```typescript
export async function executarQuery(input: { sql: string }) {
if (!isConnected()) {
return {
content: [
{ type: "text" as const, text: "⚠️ 请先使用conectar_banco。"},
],
isError: true,
};
}
try {
const result = await query(input.sql.trim());
if (result.rows.length === 0) {
return {
content: [
{
type: "text" as const,
text: "✅ 查询执行 - 未返回任何行。",
},
],
};
}
// 格式化结果为Markdown表格
const columns = Object.keys(result.rows[0]);
const header = `| ${columns.join(" | ")} |`;
const separator = `| ${columns.map(() => "---").join(" | ")} |`;
const rows = result.rows
.map(
(row) =>
`| ${columns.map((col) => String(row[col] ?? "")).join(" | ")} |`,
)
.join("\n");
return {
content: [
{
type: "text" as const,
text: `✅ ${result.rowCount} 行\n\n${header}\n${separator}\n${rows}`,
},
],
};
} catch (err) {
return {
content: [{ type: "text" as const, text: `❌ SQL 错误:${err}` }],
isError: true,
};
}
}
```
#### 工具 4 - `listar_schema`
返回完整的格式化模式,包括所有类型和外键:
```typescript
export async function listarSchema(_input: Record<string, unknown>) {
if (!isConnected()) {
return {
content: [
{ type: "text" as const, text: "⚠️ 请先使用conectar_banco。"},
],
isError: true,
};
}
// 读取information_schema中的表、列和外键
// 分组并格式化为:
// ## professors
// id integer NOT NULL
// nome character varying NOT NULL
// ↳ departamento_id → departamentos(id)
const schema = /* ... 分组和格式化 ... */ "";
return {
content: [{ type: "text" as const, text: `# 模式\n\n${schema}` }],
};
}
```
---
### 🐳 第 7 步 - 使用Podman的容器
#### 7.1 - Dockerfile
`mcp-server/Dockerfile`定义了如何构建服务器镜像:
```dockerfile
FROM node:20-alpine
# curl用于内部健康检查
RUN apk add --no-cache curl
WORKDIR /app
# 复制lockfile以利用Podman的层缓存
COPY package*.json ./
RUN npm ci # 安装package-lock.json中指定的内容
COPY tsconfig.json ./
COPY src ./src
EXPOSE 3001
# 每15秒检查一次服务器是否响应
HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
CMD curl -f http://localhost:3001/health || exit 1
# 直接执行TypeScript(无需预编译)
CMD ["node_modules/.bin/tsx", "src/index.ts"]
```
**为什么使用`npm ci`而不是`npm install`?**
- `npm ci`精确安装`package-lock.json`中指定的内容 - 可复制构建
- 更快(不解决版本冲突)
- 如果lockfile不存在或过时,则失败(问题信号)
> **重要规则**:始终在修改Dockerfile之前本地执行`npm install`以生成`package-lock.json`。`npm ci`需要lockfile。
#### 7.2 - `compose.yml`
`compose.yml`编排了三个服务并定义了它们之间的通信:
```yaml
services:
# 索引模式的向量数据库
chromadb:
image: chromadb/chroma:0.6.3
ports:
- "8000:8000" # 主机8000 → 容器8000
volumes:
- chroma_data:/chroma/chroma # 持久化数据
environment:
- ANONYMIZED_TELEMETRY=False
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s # 启动后等待30秒
# MCP服务器 - 仅在ChromaDB健康时启动
mcp-server:
build: ./mcp-server # 从Dockerfile构建
ports:
- "3001:3001"
environment:
- PORT=3001
- CHROMADB_URL=http://chromadb:8000 # Podman内部DNS
- GEMINI_API_KEY=${GEMINI_API_KEY} # 将.env文件中的密钥传递到容器
- DB_SSL=${DB_SSL:-false}
depends_on:
chromadb:
condition: service_healthy # 等待ChromaDB健康
restart: unless-stopped
# 示例数据库(可选 - 启用demo配置文件)
postgres-demo:
image: postgres:16-alpine
profiles:
- demo
ports:
- "5433:5432" # 主机5433,容器5432(PostgreSQL默认端口)
environment:
- POSTGRES_DB=demo_giu
- POSTGRES_USER=demo
- POSTGRES_PASSWORD=demo123
volumes:
- ./demo-db/init.sql:/docker-entrypoint-initdb.d/init.sql
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U demo -d demo_giu"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
volumes:
chroma_data: # ChromaDB数据
postgres_data: # 示例PostgreSQL数据
```
**Compose关键概念:
| 概念 | 功能 |
| --- | --- |
| `ports: "5433:5432"` | 将主机端口5433映射到容器端口5432 |
| `volumes:` | 持久化数据(否则全部丢失) |
| `depends_on: condition: service_healthy` | 确保启动顺序 |
| `profiles: [demo]` | 可选服务 - 仅在`--profile demo`时启动 |
| `restart: unless-stopped` | 自动重启 |
#### 7.3 - 关键命令
```bash
# 启动所有服务(后台)
podman compose up -d
# 启动示例数据库
podman compose --profile demo up -d
# 查看容器状态和健康状况
podman compose ps
# 实时日志
podman compose logs -f mcp-server
podman compose logs -f chromadb
# 重建镜像
podman compose build
podman compose up -d
# 停止容器(保留数据卷)
podman compose down
# 停止并删除所有数据卷(数据丢失)
podman compose down -v
```
---
### 🤖 第 8 步 - 集成和测试
启动所有服务并测试完整系统。
#### 8.1 - 健康检查
```bash
# 确认服务就绪
curl http://localhost:3001/health
# → {"status":"ok","service":"smartgiu-mcp-server"}
curl http://localhost:8000/api/v1/heartbeat
# → {"nanosecond heartbeat":1234567890}
podman compose ps
# 所有服务都应处于健康状态
```
#### 8.2 - 配置Claude Desktop
将以下内容添加到Claude Desktop配置文件:
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
**Linux:** `~/.config/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"smartgil": {
"type": "sse",
"url": "http://localhost:3001/sse"
}
}
```
重启Claude Desktop。工具图标(🔧)应出现在界面中 - 点击它以查看列出的4个工具。
#### 8.3 - 完整会话
**步骤 1 - 连接到示例数据库:
```
连接到数据库:主机 postgres-demo,端口 5432,
数据库 demo_giu,用户名 demo,密码 demo123
```
> 为什么使用`postgres-demo`而不是`localhost`?mcp-server在容器内运行。对于它来说,`localhost`是容器本身。示例数据库是另一个容器,通过服务名称`postgres-demo`可访问。
**步骤 2 - 探索模式:
- _"有哪些表及其存储内容?_
- _"显示完整模式"_
**步骤 3 - 提出问题:
- _"哪些教授在星期四下午有空闲时间?_
- _"哪些课程容量最接近最大值?_
- _"2025.1有多少门课程由每位教授教授?_
- "_是否存在未分配课程的课程?_"
---
## 🦙 替代方案:Ollama(本地LLM)
Ollama是一个令人惊奇的工具,允许 **完全离线运行** 模型 - 在您的计算机上,无需发送数据到云端,无需按令牌付费,无需依赖互联网。
> **为什么不在工作坊中使用?** 在活动环境中,不可能在提供的设备上安装Ollama。对于工作坊和有网络限制的企业环境,Gemini API(仅需文本密钥)更简单配置。
### Ollama的优势
- **完全隐私** - 您的所有数据都保留在您的计算机上
- **离线运行** - 无需互联网依赖
- **无请求限制** - 可以运行任意数量的查询
- **多个模型** - 嵌入式聊天,代码等。
### 如何将Gemini API替换为Ollama
**1. 安装Ollama和模型:
```bash
# 安装: https://ollama.com
# 安装模型(~274 MB)- 等效于gemini-embedding-2
ollama pull nomic-embed-text
# 模型用于ollama-client (~8.9 GB)
ollama pull gemma4
# 检查安装的模型
ollama list
# 确认服务器正在运行
curl http://localhost:11434/api/tags
```
**2. 修改`mcp-server/src/chroma-client.ts`:**
用Ollama调用替换`getEmbeddingsBatch`函数:
```typescript
// Ollama版本(替换Gemini版本)
const OLLAMA_HOST =
process.env.OLLAMA_HOST ?? "http://host.containers.internal:11434";
const EMBED_MODEL = process.env.EMBED_MODEL ?? "nomic-embed-text";
async function getEmbedding(text: string): Promise<number[]> {
const res = await fetch(`${OLLAMA_HOST}/api/embeddings`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ model: EMBED_MODEL, prompt: text }),
});
if (!res.ok) {
const body = await res.text();
throw new Error(`Ollama embedding error (${res.status}): ${body}`);
}
const data = (await res.json()) as { embedding: number[] };
return data.embedding; // 768 dimensions with nomic-embed-text(与gemini-embedding-2不同,有3072个维度)
}
```
**3. 更新`.env`:**
```env
# 将GEMINI_API_KEY替换为以下变量
OLLAMA_HOST=http://host.containers.internal:11434
EMBED_MODEL=nomic-embed-text
DB_SSL=false
```
**4. 更新`compose.yml`:**
```yaml
environment:
- PORT=3001
- CHROMADB_URL=http://chromadb:8000
- OLLAMA_HOST=${OLLAMA_HOST:-http://host.containers.internal:11434}
- EMBED_MODEL=${EMBED_MODEL:-nomic-embed-text}
- DB_SSL=${DB_SSL:-false}
```
> **为什么使用`host.containers.internal`?** mcp-server在容器内运行。对于它来说,`localhost`是容器本身。示例数据库是另一个容器,可以通过特殊的Podman主机名访问。
**5. 使用`ollama-client`(终端聊天):**
该项目包含一个完整的聊天客户端,使用Ollama作为LLM:
```bash
cd ollama-client
npm install
npm start
```
预期输出:
```
🧠 SmartGIU - 与数据库对话
MCP服务器:http://localhost:3001/sse
Ollama:http://localhost:11434 模型:gemma4
✅ MCP已连接 - 工具:conectar_banco,buscar_informacoes,executar_query,listar_schema
💬 输入您的问题(或“退出”结束):
您:连接数据库:主机postgres-demo,端口5432,
数据库demo_giu,用户demo,密码demo123
SmartGIL: 🔧 conectar_banco... ✓
✅ 已连接到“demo_giu” - 6个表已索引。
您:哪些教授有空于星期三下午?
SmartGIL: 🔧 buscar_informacoes... ✓
🔧 executar_query... ✓
可用的教授星期三下午:
• 安娜·利马(计算机科学)
• 费利佩·努内斯(工程)- 14:00至18:00
```
---
## 最佳实践
### 安全
- **.env中的API密钥**:`GEMINI_API_KEY`位于`.env`中,被`.gitignore`忽略。永远不要在存储库中提交密钥。
- **运行时内存中的数据库凭证**:密码永远不会被持久化到磁盘上 - 只在`mcp-server`进程运行时存在。每次重启都需要通过`conectar_banco`进行新的身份验证。
- **SSL**:在生产环境中使用`DB_SSL=true`连接到远程数据库。SmartGIL接受自签名证书以简化配置。
- **最小权限**:PostgreSQL用户在生产环境中应仅具有`SELECT`权限 - 除非工具需要,否则永远不要`DROP`、`TRUNCATE`或`INSERT`。
### 容器与构建
- **锁定文件必需**: 在修改 `Dockerfile` 之前,请始终运行 `npm install` 以更新 `package-lock.json`。如果没有更新锁定文件,`npm ci` 将失败。
- **修改后重建**: 修改代码后,请运行 `podman compose build`,然后运行 `podman compose up -d`。
- **健康检查**: ChromaDB 的正确端点是 `/api/v1/heartbeat`。请勿使用 `/api/v2` - 它在稳定镜像中不存在,会导致容器状态变为 `unhealthy`。
### 开发
- **扩展名 `.js`**: 在 TypeScript 中使用 `"module": "NodeNext"` 时,所有内部导入都必须使用 `.js`,即使文件是 `.ts`。
- **本地热重载**:不使用容器进行开发,请在 `mcp-server` 中运行 `` - `tsx` 会在每次更改时自动重新编译。
日志**: 使用 `podman compose logs -f mcp-server` 进行实时调试。
### 快速诊断
```bash
# 环境状态
podman compose ps
# 服务健康状况
curl http://localhost:3001/health
curl http://localhost:8000/api/v1/heartbeat
# 从零重启(删除数据 compose down -vman compose --profile demo up -d
```
---
## 🧑🏻💻 作者
| [<img loading="lazy" src="https://avatars.githubusercontent.com/u/96635074?v=4" width=br><sub>阿图尔恩特波</sub>](https://github.com/ArturCol | [<img loading="lazy="https://avatars.githubusercontent.com/u758073?v=4" width=115><br><sub>克劳迪奥·奥古斯托</sub>](https://github.com/claudiogpt) |
| :---: | :---: |
---
## 许可证项目根据 [MIT 许可证://opensource.org/licenses/M 授权。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器