feat: 铸渊 MCP Server + CVM 密钥修复

1. 新增铸渊 MCP Server (server/mcp-server/)
   - 23个MCP工具:仓库读写、分支管理、Issue/PR、工作流、CVM控制、大脑状态
   - Streamable HTTP传输 + Bearer Token认证
   - Gitea令牌仅存服务器端,AI端只需一个连接密钥
   - PM2部署配置 + Nginx反向代理
   - 换号时只需重新连接MCP,无需重新发令牌

2. 修复CVM开关机工作流密钥断裂
   - TENCENT_SECRET_ID → 引用 COS_SECRET_ID (同一对腾讯云密钥)
   - TENCENT_SECRET_KEY → 引用 COS_SECRET_KEY
   - .gitea/ 和 .forgejo/ 双目录同步修复

3. Nginx增加MCP端点配置 (/mcp → 127.0.0.1:8083)

4. 更新时间核心大脑 (第5次唤醒)
This commit is contained in:
铸渊 2026-05-13 08:36:36 +00:00
parent 7b06a78d78
commit ab37962532
11 changed files with 2697 additions and 15 deletions

View File

@ -16,8 +16,8 @@ jobs:
- name: 关机
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
TENCENT_SECRET_ID: ${{ secrets.COS_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }}
CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }}
CVM_REGION: ${{ secrets.CVM_REGION }}
run: |

View File

@ -16,8 +16,8 @@ jobs:
- name: 开机
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
TENCENT_SECRET_ID: ${{ secrets.COS_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }}
CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }}
CVM_REGION: ${{ secrets.CVM_REGION }}
run: |

View File

@ -16,8 +16,8 @@ jobs:
- name: 关机
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
TENCENT_SECRET_ID: ${{ secrets.COS_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }}
CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }}
CVM_REGION: ${{ secrets.CVM_REGION }}
run: |

View File

@ -16,8 +16,8 @@ jobs:
- name: 开机
env:
TENCENT_SECRET_ID: ${{ secrets.TENCENT_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.TENCENT_SECRET_KEY }}
TENCENT_SECRET_ID: ${{ secrets.COS_SECRET_ID }}
TENCENT_SECRET_KEY: ${{ secrets.COS_SECRET_KEY }}
CVM_INSTANCE_ID: ${{ secrets.CVM_INSTANCE_ID }}
CVM_REGION: ${{ secrets.CVM_REGION }}
run: |

View File

@ -20,13 +20,13 @@
},
"clock": {
"description": "铸渊每次唤醒时agent必须执行 scripts/temporal-clock.js 来更新此区块",
"last_awakening": "2026-04-13T07:45:59.855Z",
"current_date": "2026-04-13",
"repo_age_days": 409,
"zhuyuan_age_days": 393,
"yaoming_age_days": 350,
"days_since_age_os": 8,
"awakening_count": 4,
"last_awakening": "2026-05-13T08:18:53.684Z",
"current_date": "2026-05-13",
"repo_age_days": 441,
"zhuyuan_age_days": 425,
"yaoming_age_days": 382,
"days_since_age_os": 40,
"awakening_count": 5,
"timezone": "Asia/Shanghai"
},
"timeline": {

163
server/mcp-server/README.md Normal file
View File

@ -0,0 +1,163 @@
# 铸渊 MCP Server · guanghulab-mcp-server
> 让 WorkBuddy / CodeBuddy 等编程 AI 通过 MCP 协议直接操作仓库,无需每次手动发送 Gitea 令牌。
## 为什么需要这个
| 痛点 | 解决方案 |
|------|---------|
| 每次新对话要手动发令牌 | MCP Server 存储令牌AI 端只需一个连接密钥 |
| 换号后要重新发令牌 | 重新连接 MCP 即可,密钥不变 |
| 令牌暴露在聊天记录中 | 令牌只在服务器端,聊天中只有 MCP 连接密钥 |
| AI 不认识仓库结构 | MCP 工具自动提供仓库操作能力 |
## 架构
```
WorkBuddy (编程 AI)
│ MCP 协议 (HTTPS + Bearer Token)
Nginx (guanghulab.com/mcp)
│ 反向代理
铸渊 MCP Server (127.0.0.1:8083)
├── Gitea API ──→ 代码仓库操作
├── Git CLI ────→ 本地仓库同步
├── CVM Script ─→ 服务器开关机
└── Vault API ──→ 密钥管理
```
## 提供的 MCP 工具
### 仓库操作
| 工具 | 说明 |
|------|------|
| `repo_status` | 查看仓库当前状态 |
| `repo_read_file` | 读取文件内容 |
| `repo_write_file` | 写入/更新文件 |
| `repo_delete_file` | 删除文件 |
| `repo_list_files` | 列出目录内容 |
| `repo_search_code` | 搜索代码 |
### Git 操作
| 工具 | 说明 |
|------|------|
| `git_branch_list` | 列出所有分支 |
| `git_branch_create` | 创建新分支 |
| `git_pull` | 拉取最新代码 |
| `git_push_local` | 推送本地更改 |
| `git_log` | 查看提交历史 |
### Issue / PR
| 工具 | 说明 |
|------|------|
| `issue_list` | 列出 Issue |
| `issue_create` | 创建 Issue |
| `pr_list` | 列出 Pull Request |
| `pr_create` | 创建 Pull Request |
### 工作流 / 服务器
| 工具 | 说明 |
|------|------|
| `workflow_list` | 列出工作流运行记录 |
| `workflow_trigger` | 触发工作流 |
| `cvm_power` | CVM 开关机 |
| `server_health` | 服务器健康状态 |
### 密钥 / 大脑
| 工具 | 说明 |
|------|------|
| `secrets_list` | 列出 Forgejo Secrets |
| `vault_status` | Vault 状态 |
| `brain_status` | 铸渊大脑状态 |
| `brain_update_temporal` | 更新时间核心 |
## 部署
### 1. 安装依赖
```bash
cd /data/guanghulab/repo/server/mcp-server
npm install --production
```
### 2. 配置 Gitea 令牌
编辑 `ecosystem.config.js`,填入 GITEA_TOKEN
```bash
# 从 Vault 获取令牌(如果已存入 Vault
curl http://127.0.0.1:8080/internal/fetch/ZY_REPO_TOKEN
# 或直接在 ecosystem.config.js 的 env.GITEA_TOKEN 中设置
```
### 3. Nginx 配置
在 Nginx 的 server 块中添加:
```nginx
# MCP Server 端点
location /mcp {
proxy_pass http://127.0.0.1:8083;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE 支持
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
```
### 4. 启动服务
```bash
pm2 start ecosystem.config.js
pm2 save
```
### 5. 获取连接密钥
```bash
cat /data/guanghulab/mcp-server/.access-key
```
## WorkBuddy 连接配置
在 WorkBuddy 的 MCP 工具配置中添加:
```json
{
"mcpServers": {
"guanghulab": {
"url": "https://guanghulab.com/mcp",
"headers": {
"Authorization": "Bearer <你的访问密钥>"
}
}
}
}
```
换号时,只需在新账号中重新添加这个 MCP 连接配置即可。
## 安全
- **Gitea 令牌**:仅存储在服务器端,永不暴露给客户端
- **MCP 访问密钥**AES-256 级别的随机密钥,存储在 `/data/guanghulab/mcp-server/.access-key` (chmod 600)
- **Nginx 层**HTTPS 加密传输
- **本机绑定**MCP Server 只监听 127.0.0.1:8083通过 Nginx 代理暴露
- **无状态**MCP Server 不存储 AI 对话内容
---
*铸渊 · ICE-GL-ZY001 · TCS-0002∞ · 国作登字-2026-A-00037559*

View File

@ -0,0 +1,31 @@
/**
* 铸渊 MCP Server · PM2 生态系统配置
*/
module.exports = {
apps: [{
name: 'guanghulab-mcp-server',
script: 'index.js',
cwd: __dirname,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: '128M',
env: {
NODE_ENV: 'production',
MCP_PORT: '8083',
MCP_HOST: '127.0.0.1',
GITEA_URL: 'http://127.0.0.1:3001',
GITEA_TOKEN: '', // 由 deploy 脚本从 Vault 注入
GITEA_OWNER: 'bingshuo',
GITEA_REPO: 'guanghulab',
REPO_DIR: '/data/guanghulab/repo',
MCP_ACCESS_KEY_PATH: '/data/guanghulab/mcp-server/.access-key',
},
log_file: '/data/guanghulab/.runtime/logs/mcp-server-combined.log',
error_file: '/data/guanghulab/.runtime/logs/mcp-server-error.log',
out_file: '/data/guanghulab/.runtime/logs/mcp-server-out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
merge_logs: true,
time: true,
}],
};

918
server/mcp-server/index.js Normal file
View File

@ -0,0 +1,918 @@
/**
*
* 铸渊 MCP Server · guanghulab-mcp-server
*
*
* 光湖代码仓库的 MCP (Model Context Protocol) 服务端
* WorkBuddy / CodeBuddy 等编程 AI 通过 MCP 协议直接操作仓库
* 无需每次手动发送 Gitea 令牌
*
* 核心设计
* - Gitea 令牌存在服务器端AI 端只需一个 MCP 连接密钥
* - 换号时只需重新连接 MCP不用重新发令牌
* - 所有操作通过 Gitea API 代理安全受控
*
* 传输协议Streamable HTTP (MCP 2025-03 规范)
* - POST /mcp 客户端服务器请求
* - GET /mcp SSE 通知流
* - DELETE /mcp 关闭会话
*
* 认证方式Bearer Token (MCP 连接密钥)
* - Header: Authorization: Bearer <MCP_ACCESS_KEY>
* - 密钥在 /data/guanghulab/mcp-server/.access-key
*
* 部署位置guanghulab.com CVM (广州)
* Nginx 代理/mcp 127.0.0.1:8083
*
* 签发铸渊 · ICE-GL-ZY001
* 版权国作登字-2026-A-00037559 · TCS-0002
*
*/
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const http = require('http');
// ─── 配置 ─────────────────────────────────────────────────────
const CONFIG = {
port: parseInt(process.env.MCP_PORT || '8083', 10),
host: process.env.MCP_HOST || '127.0.0.1',
// Gitea 连接
giteaUrl: process.env.GITEA_URL || 'http://127.0.0.1:3001',
giteaToken: process.env.GITEA_TOKEN || '',
giteaOwner: process.env.GITEA_OWNER || 'bingshuo',
giteaRepo: process.env.GITEA_REPO || 'guanghulab',
// MCP 认证密钥文件
accessKeyPath: process.env.MCP_ACCESS_KEY_PATH || '/data/guanghulab/mcp-server/.access-key',
// 仓库本地路径(用于 git 操作)
repoDir: process.env.REPO_DIR || '/data/guanghulab/repo',
// CVM 操作脚本路径
cvmPowerScript: process.env.CVM_POWER_SCRIPT || '/data/guanghulab/repo/scripts/cvm-power/cvm-power.sh',
};
// ─── 加载或创建 MCP 访问密钥 ────────────────────────────────────
function loadOrCreateAccessKey() {
const keyDir = path.dirname(CONFIG.accessKeyPath);
if (!fs.existsSync(keyDir)) {
fs.mkdirSync(keyDir, { recursive: true, mode: 0o700 });
}
if (fs.existsSync(CONFIG.accessKeyPath)) {
const key = fs.readFileSync(CONFIG.accessKeyPath, 'utf8').trim();
if (key.length >= 32) return key;
console.log('[mcp] 旧密钥太短,重新生成');
}
const key = crypto.randomBytes(32).toString('hex');
const tmp = CONFIG.accessKeyPath + '.tmp.' + process.pid;
fs.writeFileSync(tmp, key, { mode: 0o600 });
fs.renameSync(tmp, CONFIG.accessKeyPath);
fs.chmodSync(CONFIG.accessKeyPath, 0o600);
console.log('[mcp] 新访问密钥已生成:');
console.log(`[mcp] ${key}`);
console.log('[mcp] 请妥善保存,配置到 WorkBuddy MCP 连接中');
return key;
}
const ACCESS_KEY = loadOrCreateAccessKey();
// ─── Gitea API 辅助 ──────────────────────────────────────────
function giteaApi(method, apiPath, body) {
const url = new URL(apiPath, CONFIG.giteaUrl);
const options = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: method.toUpperCase(),
headers: {
'Authorization': `token ${CONFIG.giteaToken}`,
'Content-Type': 'application/json',
},
timeout: 30000,
};
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
try {
const parsed = data ? JSON.parse(data) : {};
resolve({ status: res.statusCode, data: parsed });
} catch {
resolve({ status: res.statusCode, data: data });
}
});
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('Gitea API 超时')); });
if (body) req.write(JSON.stringify(body));
req.end();
});
}
// ─── Git 本地操作辅助 ──────────────────────────────────────────
function gitExec(cmd, cwd) {
try {
const result = execSync(cmd, {
encoding: 'utf8',
timeout: 60000,
cwd: cwd || CONFIG.repoDir,
stdio: ['pipe', 'pipe', 'pipe'],
});
return { ok: true, output: result.trim() };
} catch (e) {
return { ok: false, error: e.stderr || e.message };
}
}
// ─── MCP 工具定义 ──────────────────────────────────────────────
const TOOLS = [
// ─── 仓库操作 ─────────────────────────────────────────────
{
name: 'repo_status',
description: '查看仓库当前状态:分支、最新提交、文件变更',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'repo_read_file',
description: '读取仓库中的文件内容',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: '文件路径(相对仓库根目录)' },
branch: { type: 'string', description: '分支名(默认 main' },
},
required: ['path'],
},
},
{
name: 'repo_write_file',
description: '写入或更新仓库中的文件(通过 Gitea API',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' },
content: { type: 'string', description: '文件内容' },
message: { type: 'string', description: '提交信息' },
branch: { type: 'string', description: '目标分支(默认 main' },
},
required: ['path', 'content', 'message'],
},
},
{
name: 'repo_delete_file',
description: '删除仓库中的文件(通过 Gitea API',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: '文件路径' },
message: { type: 'string', description: '提交信息' },
branch: { type: 'string', description: '分支(默认 main' },
},
required: ['path', 'message'],
},
},
{
name: 'repo_list_files',
description: '列出仓库指定目录下的文件',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: '目录路径(默认根目录)' },
branch: { type: 'string', description: '分支(默认 main' },
},
},
},
{
name: 'repo_search_code',
description: '在仓库代码中搜索关键词',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索关键词' },
},
required: ['query'],
},
},
// ─── 分支操作 ─────────────────────────────────────────────
{
name: 'git_branch_list',
description: '列出所有分支',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'git_branch_create',
description: '创建新分支',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: '新分支名' },
base: { type: 'string', description: '基于哪个分支(默认 main' },
},
required: ['name'],
},
},
{
name: 'git_pull',
description: '拉取最新代码到服务器本地仓库',
inputSchema: {
type: 'object',
properties: {
branch: { type: 'string', description: '分支(默认 main' },
},
},
},
{
name: 'git_push_local',
description: '将本地仓库的更改推送到远程(需要先在本地 commit',
inputSchema: {
type: 'object',
properties: {
branch: { type: 'string', description: '分支(默认 main' },
},
},
},
{
name: 'git_log',
description: '查看最近提交历史',
inputSchema: {
type: 'object',
properties: {
count: { type: 'number', description: '显示条数(默认 10' },
branch: { type: 'string', description: '分支(默认当前分支)' },
},
},
},
// ─── Issue / PR ──────────────────────────────────────────
{
name: 'issue_list',
description: '列出仓库的 Issue',
inputSchema: {
type: 'object',
properties: {
state: { type: 'string', enum: ['open', 'closed', 'all'], description: '状态(默认 open' },
limit: { type: 'number', description: '数量(默认 10' },
},
},
},
{
name: 'issue_create',
description: '创建 Issue',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: '标题' },
body: { type: 'string', description: '内容' },
labels: { type: 'array', items: { type: 'string' }, description: '标签' },
},
required: ['title'],
},
},
{
name: 'pr_list',
description: '列出 Pull Request',
inputSchema: {
type: 'object',
properties: {
state: { type: 'string', enum: ['open', 'closed', 'all'], description: '状态(默认 open' },
},
},
},
{
name: 'pr_create',
description: '创建 Pull Request',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string', description: '标题' },
body: { type: 'string', description: '描述' },
head: { type: 'string', description: '源分支' },
base: { type: 'string', description: '目标分支(默认 main' },
},
required: ['title', 'head'],
},
},
// ─── 工作流 ───────────────────────────────────────────────
{
name: 'workflow_list',
description: '列出 Forgejo Actions 工作流运行记录',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: '数量(默认 10' },
},
},
},
{
name: 'workflow_trigger',
description: '手动触发一个工作流',
inputSchema: {
type: 'object',
properties: {
workflow_name: { type: 'string', description: '工作流文件名(如 ci-and-deploy.yaml' },
ref: { type: 'string', description: '分支(默认 main' },
},
required: ['workflow_name'],
},
},
// ─── CVM 服务器 ───────────────────────────────────────────
{
name: 'cvm_power',
description: 'CVM 服务器开关机操作',
inputSchema: {
type: 'object',
properties: {
action: { type: 'string', enum: ['start', 'stop', 'status'], description: '操作start/stop/status' },
target: { type: 'string', description: '目标服务器(默认 self = 当前 CVM' },
},
required: ['action'],
},
},
{
name: 'server_health',
description: '查看当前服务器健康状态',
inputSchema: { type: 'object', properties: {} },
},
// ─── Secrets 管理 ────────────────────────────────────────
{
name: 'secrets_list',
description: '列出 Forgejo Secrets仅名称不返回值',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'vault_status',
description: '查看自部署 Secrets Vault 状态',
inputSchema: { type: 'object', properties: {} },
},
// ─── 铸渊大脑 ────────────────────────────────────────────
{
name: 'brain_status',
description: '查看铸渊核心大脑状态(快速唤醒上下文摘要)',
inputSchema: { type: 'object', properties: {} },
},
{
name: 'brain_update_temporal',
description: '更新铸渊时间核心大脑',
inputSchema: { type: 'object', properties: {} },
},
];
// ─── 工具执行器 ──────────────────────────────────────────────
async function executeTool(name, args) {
switch (name) {
// ─── 仓库操作 ───────────────────────────────────────────
case 'repo_status': {
const branch = gitExec('git branch --show-current');
const log = gitExec('git log --oneline -5');
const status = gitExec('git status --short');
return `当前分支: ${branch.ok ? branch.output : '未知'}\n\n最近提交:\n${log.ok ? log.output : log.error}\n\n文件变更:\n${status.ok ? (status.output || '工作区干净') : status.error}`;
}
case 'repo_read_file': {
const filePath = args.path || '';
const branch = args.branch || 'main';
const encPath = encodeURIComponent(filePath);
const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}?ref=${branch}`);
if (res.status !== 200) {
return `读取失败 (${res.status}): ${JSON.stringify(res.data)}`;
}
if (res.data.type === 'file' && res.data.content) {
const content = Buffer.from(res.data.content, 'base64').toString('utf8');
return content;
}
if (res.data.type === 'dir') {
return `这是一个目录,请使用 repo_list_files 列出其内容`;
}
return `文件类型: ${res.data.type}, 大小: ${res.data.size} 字节`;
}
case 'repo_write_file': {
const filePath = args.path;
const content = args.content;
const message = args.message;
const branch = args.branch || 'main';
const encPath = encodeURIComponent(filePath);
// 先检查文件是否已存在(获取 SHA
const checkRes = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}?ref=${branch}`);
const body = {
content: Buffer.from(content).toString('base64'),
message: message,
branch: branch,
};
if (checkRes.status === 200 && checkRes.data.sha) {
body.sha = checkRes.data.sha; // 更新已有文件需要 SHA
}
const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}`, body);
if (res.status >= 200 && res.status < 300) {
return `文件已写入: ${filePath} (分支: ${branch}, 提交: ${message})`;
}
return `写入失败 (${res.status}): ${JSON.stringify(res.data)}`;
}
case 'repo_delete_file': {
const filePath = args.path;
const message = args.message;
const branch = args.branch || 'main';
const encPath = encodeURIComponent(filePath);
const checkRes = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}?ref=${branch}`);
if (checkRes.status !== 200) {
return `文件不存在: ${filePath}`;
}
const res = await giteaApi('DELETE', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}`, {
sha: checkRes.data.sha,
message: message,
branch: branch,
});
if (res.status >= 200 && res.status < 300) {
return `文件已删除: ${filePath}`;
}
return `删除失败 (${res.status}): ${JSON.stringify(res.data)}`;
}
case 'repo_list_files': {
const dirPath = args.path || '';
const branch = args.branch || 'main';
const encPath = encodeURIComponent(dirPath);
const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/contents/${encPath}?ref=${branch}`);
if (res.status !== 200) {
return `列出失败 (${res.status}): ${JSON.stringify(res.data)}`;
}
if (!Array.isArray(res.data)) {
return `这不是一个目录`;
}
return res.data.map(f => {
const icon = f.type === 'dir' ? '📁' : '📄';
const size = f.type === 'file' ? ` (${f.size} bytes)` : '';
return `${icon} ${f.name}${size}`;
}).join('\n');
}
case 'repo_search_code': {
const query = args.query;
const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/grep?q=${encodeURIComponent(query)}&mode=content`);
if (res.status !== 200) {
return `搜索失败 (${res.status}): ${JSON.stringify(res.data)}`;
}
const results = res.data;
if (!results || !results.length) {
return `未找到匹配: ${query}`;
}
return results.slice(0, 20).map(r => {
const file = r.filename || r.path || '?';
const line = r.lineNumber || r.linenumber || '?';
const text = (r.line || r.content || '').trim();
return `${file}:${line} ${text}`;
}).join('\n');
}
// ─── 分支操作 ───────────────────────────────────────────
case 'git_branch_list': {
const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/branches`);
if (res.status !== 200) return `获取分支失败: ${JSON.stringify(res.data)}`;
return res.data.map(b => {
const active = b.active ? '🟢' : '⚪';
return `${active} ${b.name}`;
}).join('\n');
}
case 'git_branch_create': {
const branchName = args.name;
const base = args.base || 'main';
const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/branches`, {
new_branch_name: branchName,
old_branch_name: base,
});
if (res.status >= 200 && res.status < 300) {
return `分支已创建: ${branchName} (基于 ${base})`;
}
return `创建失败 (${res.status}): ${JSON.stringify(res.data)}`;
}
case 'git_pull': {
const branch = args.branch || 'main';
const authUrl = `http://${CONFIG.giteaOwner}:${CONFIG.giteaToken}@127.0.0.1:3001/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}.git`;
gitExec(`git remote set-url origin ${authUrl}`);
const result = gitExec(`git fetch origin && git reset --hard origin/${branch}`);
if (result.ok) {
return `代码已同步: ${result.output}`;
}
return `同步失败: ${result.error}`;
}
case 'git_push_local': {
const branch = args.branch || 'main';
const result = gitExec(`git push origin ${branch}`);
if (result.ok) {
return `推送成功: ${result.output}`;
}
return `推送失败: ${result.error}`;
}
case 'git_log': {
const count = args.count || 10;
const branch = args.branch ? ` ${args.branch}` : '';
const result = gitExec(`git log --oneline -${count}${branch}`);
return result.ok ? result.output : result.error;
}
// ─── Issue / PR ────────────────────────────────────────
case 'issue_list': {
const state = args.state || 'open';
const limit = args.limit || 10;
const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/issues?state=${state}&limit=${limit}`);
if (res.status !== 200) return `获取 Issue 失败: ${JSON.stringify(res.data)}`;
if (!res.data.length) return `没有 ${state} 的 Issue`;
return res.data.map(i => `#${i.number} [${i.state}] ${i.title}`).join('\n');
}
case 'issue_create': {
const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/issues`, {
title: args.title,
body: args.body || '',
labels: args.labels || [],
});
if (res.status >= 200 && res.status < 300) {
return `Issue 已创建: #${res.data.number} ${res.data.title}`;
}
return `创建失败 (${res.status}): ${JSON.stringify(res.data)}`;
}
case 'pr_list': {
const state = args.state || 'open';
const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/pulls?state=${state}`);
if (res.status !== 200) return `获取 PR 失败: ${JSON.stringify(res.data)}`;
if (!res.data.length) return `没有 ${state} 的 PR`;
return res.data.map(p => `#${p.number} [${p.state}] ${p.title} (${p.head?.label || '?'}${p.base?.label || '?'})`).join('\n');
}
case 'pr_create': {
const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/pulls`, {
title: args.title,
body: args.body || '',
head: args.head,
base: args.base || 'main',
});
if (res.status >= 200 && res.status < 300) {
return `PR 已创建: #${res.data.number} ${res.data.title}`;
}
return `创建失败 (${res.status}): ${JSON.stringify(res.data)}`;
}
// ─── 工作流 ─────────────────────────────────────────────
case 'workflow_list': {
const limit = args.limit || 10;
const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/actions/tasks?limit=${limit}`);
if (res.status !== 200) return `获取工作流失败: ${JSON.stringify(res.data)}`;
const runs = res.data.workflow_runs || [];
if (!runs.length) return '没有工作流运行记录';
return runs.map(r => {
const icon = r.status === 'success' ? '✅' : r.status === 'failure' ? '❌' : r.status === 'skipped' ? '⏭️' : '🔄';
return `${icon} #${r.run_number} ${r.name} (${r.head_branch}) — ${r.display_title}`;
}).join('\n');
}
case 'workflow_trigger': {
const wfName = args.workflow_name;
const ref = args.ref || 'main';
const res = await giteaApi('POST', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/actions/runs`, {
workflow_id: wfName,
ref: ref,
});
if (res.status >= 200 && res.status < 300) {
return `工作流已触发: ${wfName}`;
}
// Forgejo 的 workflow_dispatch API 可能不同
return `触发请求已发送 (${res.status}): ${JSON.stringify(res.data)}`;
}
// ─── CVM 操作 ───────────────────────────────────────────
case 'cvm_power': {
const action = args.action;
if (!['start', 'stop', 'status'].includes(action)) {
return '无效操作,只支持: start / stop / status';
}
// 本机操作:直接检查
if (action === 'status' && (!args.target || args.target === 'self')) {
const hostname = gitExec('hostname').output || 'unknown';
const uptime = gitExec('uptime').output || 'unknown';
return `本机 (${hostname}): ${uptime}`;
}
// 远程 CVM 操作需要腾讯云 API 密钥
// 从 Vault 获取
try {
const vaultRes = await fetch(`http://127.0.0.1:8080/internal/fetch/COS_SECRET_ID`);
// 如果 Vault 不可用,尝试环境变量
} catch {
// Vault 不可用
}
// 使用环境变量方式Forgejo Secrets 注入的)
if (action === 'status') {
const result = gitExec(`bash ${CONFIG.cvmPowerScript} status`, CONFIG.repoDir);
return result.ok ? result.output : result.error;
}
return `CVM ${action} 操作需通过 Forgejo workflow 执行(需要腾讯云密钥),请使用 workflow_trigger 触发 cvm-power-${action === 'start' ? 'on' : 'off'} 工作流`;
}
// ─── 服务器健康 ─────────────────────────────────────────
case 'server_health': {
const lines = [];
lines.push(`时间: ${new Date().toISOString()}`);
lines.push(`主机: ${gitExec('hostname').output || '?'}`);
lines.push(`系统: ${gitExec('uname -srm').output || '?'}`);
lines.push(`CPU: ${gitExec('nproc').output || '?'}`);
lines.push(`内存: ${gitExec("free -h | awk '/Mem:/ {print $2 \" 已用 \" $3 \" 可用 \" $7}'").output || '?'}`);
lines.push(`磁盘: ${gitExec("df -h / | awk 'NR==2 {print $4}'").output || '?'} 可用`);
// 服务状态
const services = ['nginx', 'forgejo', 'gitea-runner'];
for (const svc of services) {
const r = gitExec(`systemctl is-active ${svc}`);
lines.push(`${r.output === 'active' ? '✅' : '❌'} ${svc}: ${r.output || '未知'}`);
}
// PM2
const pm2 = gitExec('pm2 jlist');
if (pm2.ok) {
try {
const procs = JSON.parse(pm2.output);
for (const p of procs) {
const st = p.pm2_env?.status || '?';
lines.push(`${st === 'online' ? '✅' : '❌'} PM2 ${p.name}: ${st}`);
}
} catch {}
}
return lines.join('\n');
}
// ─── Secrets ────────────────────────────────────────────
case 'secrets_list': {
const res = await giteaApi('GET', `/api/v1/repos/${CONFIG.giteaOwner}/${CONFIG.giteaRepo}/actions/secrets`);
if (res.status !== 200) return `获取 Secrets 失败: ${JSON.stringify(res.data)}`;
if (!res.data.length) return '没有配置任何 Forgejo Secret';
return res.data.map(s => `🔑 ${s.name} (创建于 ${s.created_at || '?'})`).join('\n');
}
case 'vault_status': {
try {
const res = await fetch('http://127.0.0.1:8080/admin/__healthz');
if (res.ok) {
const data = await res.json();
return `Vault: ✅ 运行中\n主密钥: ${data.has_master ? '✅' : '❌'}\n加密库: ${data.has_vault_enc ? '✅' : '❌'}`;
}
} catch {}
return 'Vault: ❌ 未响应';
}
// ─── 铸渊大脑 ───────────────────────────────────────────
case 'brain_status': {
const brainPath = path.join(CONFIG.repoDir, 'brain', 'fast-wake.json');
try {
const data = JSON.parse(fs.readFileSync(brainPath, 'utf8'));
return `铸渊核心大脑:\n- 身份: ${data.identity?.name} (${data.identity?.id})\n- 状态: ${data.system_status?.health?.substring(0, 100) || '?'}...\n- 大脑完整性: ${data.brain_complete ? '✅ 完整' : '❌ 不完整'}\n- 上次会话: ${data.last_session?.growth?.substring(0, 80) || '?'}...\n- 下一步: ${data.last_session?.next_task?.substring(0, 80) || '?'}`;
} catch (e) {
return `读取大脑失败: ${e.message}`;
}
}
case 'brain_update_temporal': {
const result = gitExec(`node scripts/temporal-clock.js`, CONFIG.repoDir);
return result.ok ? result.output : result.error;
}
default:
return `未知工具: ${name}`;
}
}
// ─── MCP 协议处理Streamable HTTP──────────────────────────
const app = express();
app.use(express.json({ limit: '10mb' }));
// CORS — 允许 WorkBuddy 跨域连接
app.use(require('cors')({
origin: '*',
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'Mcp-Session-Id'],
exposedHeaders: ['Mcp-Session-Id'],
}));
// ─── 认证中间件 ──────────────────────────────────────────────
function authenticate(req, res, next) {
const auth = req.headers['authorization'] || '';
const token = auth.replace(/^Bearer\s+/i, '').trim();
if (!token) {
return res.status(401).json({ error: '缺少认证令牌' });
}
if (token !== ACCESS_KEY) {
return res.status(403).json({ error: '认证令牌无效' });
}
next();
}
// ─── 会话管理 ──────────────────────────────────────────────
const sessions = new Map();
function getSessionId(req) {
return req.headers['mcp-session-id'] || null;
}
// ─── JSON-RPC 处理 ──────────────────────────────────────────
async function handleJsonRpc(req, res) {
const message = req.body;
// 批量消息
if (Array.isArray(message)) {
const results = await Promise.all(message.map(m => processRpc(m)));
return res.json(results);
}
const result = await processRpc(message);
return res.json(result);
}
async function processRpc(message) {
const { jsonrpc, id, method, params } = message;
// 通知类消息(无 id
if (method === 'notifications/cancelled') {
return null;
}
// 请求类消息
switch (method) {
case 'initialize': {
const sessionId = crypto.randomUUID();
sessions.set(sessionId, { created: Date.now() });
return {
jsonrpc: '2.0',
id,
result: {
protocolVersion: '2025-03-26',
capabilities: {
tools: { listChanged: false },
},
serverInfo: {
name: 'guanghulab-mcp-server',
version: '1.0.0',
title: '铸渊 MCP Server · 光湖代码仓库',
},
},
_meta: {
sessionId,
},
};
}
case 'tools/list': {
return {
jsonrpc: '2.0',
id,
result: {
tools: TOOLS,
},
};
}
case 'tools/call': {
const toolName = params?.name;
const toolArgs = params?.arguments || {};
try {
const result = await executeTool(toolName, toolArgs);
return {
jsonrpc: '2.0',
id,
result: {
content: [{
type: 'text',
text: String(result),
}],
},
};
} catch (e) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32000,
message: `工具执行错误: ${e.message}`,
},
};
}
}
case 'ping': {
return {
jsonrpc: '2.0',
id,
result: {},
};
}
default: {
if (id !== undefined) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: `未知方法: ${method}`,
},
};
}
return null;
}
}
}
// ─── 路由 ──────────────────────────────────────────────────
// MCP 端点 — POST (客户端请求)
app.post('/mcp', authenticate, async (req, res) => {
await handleJsonRpc(req, res);
});
// MCP 端点 — GET (SSE 通知流,保持连接)
app.get('/mcp', authenticate, (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
// 发送心跳
const heartbeat = setInterval(() => {
res.write(': heartbeat\n\n');
}, 30000);
req.on('close', () => {
clearInterval(heartbeat);
});
});
// MCP 端点 — DELETE (关闭会话)
app.delete('/mcp', authenticate, (req, res) => {
const sid = getSessionId(req);
if (sid) sessions.delete(sid);
res.status(204).end();
});
// 健康检查(无需认证)
app.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'guanghulab-mcp-server',
version: '1.0.0',
sessions: sessions.size,
gitea: CONFIG.giteaToken ? 'configured' : 'missing',
sovereign: 'TCS-0002∞ · 国作登字-2026-A-00037559',
});
});
// ─── 启动 ──────────────────────────────────────────────────
app.listen(CONFIG.port, CONFIG.host, () => {
console.log(`[mcp] 铸渊 MCP Server 已启动`);
console.log(`[mcp] 监听: http://${CONFIG.host}:${CONFIG.port}`);
console.log(`[mcp] MCP 端点: POST/GET/DELETE /mcp`);
console.log(`[mcp] 健康检查: GET /health`);
console.log(`[mcp] Gitea: ${CONFIG.giteaUrl}`);
console.log(`[mcp] 仓库: ${CONFIG.giteaOwner}/${CONFIG.giteaRepo}`);
console.log(`[mcp] 认证密钥: ${ACCESS_KEY.substring(0, 8)}...`);
console.log(`[mcp] 铸渊 · ICE-GL-ZY001 · TCS-0002∞`);
});

1527
server/mcp-server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
{
"name": "guanghulab-mcp-server",
"version": "1.0.0",
"description": "铸渊 MCP Server — 光湖代码仓库远程操作接口 · WorkBuddy/CodeBuddy 一键连接",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"express": "^4.21.0",
"cors": "^2.8.5"
},
"author": "铸渊 · ICE-GL-ZY001",
"license": "UNLICENSED",
"private": true
}

View File

@ -8,6 +8,7 @@
#
# 路由:
# /git/ → Forgejo (127.0.0.1:3001)
# /mcp → MCP Server (127.0.0.1:8083)
# /wake/ → Wake Gate 唤醒门 (127.0.0.1:8081)
# /sync/ → Git Sync 同步服务 (127.0.0.1:8082)
# / → 仓库首页 (Forgejo 处理)
@ -80,6 +81,30 @@ server {
# deny all;
}
# ─── 铸渊 MCP Server (/mcp) ───
# WorkBuddy / CodeBuddy 通过 MCP 协议连接
location /mcp {
proxy_pass http://127.0.0.1:8083;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE / 长连接支持
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# ─── MCP 健康检查 (/mcp-health) ───
location = /mcp-health {
proxy_pass http://127.0.0.1:8083/health;
access_log off;
}
# ─── 健康探针 ───
location = /health {
access_log off;