nx-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +11 -0
- package/README.md +168 -0
- package/config.json +7 -0
- package/package.json +27 -0
- package/src/index.js +343 -0
- package/src/providers/tongyi.js +153 -0
- package/src/tools/barcode.js +176 -0
- package/src/tools/basic.js +121 -0
- package/src/tools/colors.js +83 -0
- package/src/tools/compress.js +31 -0
- package/src/tools/convert.js +48 -0
- package/src/tools/qrcode.js +80 -0
- package/src/tools/smart.js +52 -0
- package/src/tools/stitch.js +68 -0
- package/src/tools/watermark.js +88 -0
- package/src/utils/gradient.js +27 -0
- package/src/utils/output.js +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# NX MCP Server
|
|
2
|
+
|
|
3
|
+
图像生成 MCP Server——智能生图(通义万相 API)+ 本地图形工具集,支持 Stdio 和 HTTP 双传输模式。
|
|
4
|
+
|
|
5
|
+
## 工具列表
|
|
6
|
+
|
|
7
|
+
### 智能生图(通义万相 API)
|
|
8
|
+
| 工具 | 说明 |
|
|
9
|
+
|------|------|
|
|
10
|
+
| `generate_smart` | 文生图,调用通义万相模型 |
|
|
11
|
+
|
|
12
|
+
### 本地图形工具(无需网络/API Key)
|
|
13
|
+
| 工具 | 说明 |
|
|
14
|
+
|------|------|
|
|
15
|
+
| `generate_basic` | 图形合成:形状/文字/图片叠加 |
|
|
16
|
+
| `generate_qrcode` | 二维码生成(支持中心 logo + 文字水印) |
|
|
17
|
+
| `generate_barcode` | 条形码生成(CODE128/CODE39/EAN13/EAN8/UPC/ITF) |
|
|
18
|
+
| `compress_image` | 图片压缩(质量/尺寸) |
|
|
19
|
+
| `convert_format` | 格式互转(PNG↔JPEG↔WebP↔BMP↔TIFF→AVIF) |
|
|
20
|
+
| `stitch_images` | 多图拼接/网格布局 |
|
|
21
|
+
| `extract_colors` | 提取图片主色调(hex/rgb/hsl) |
|
|
22
|
+
| `add_watermark` | 文字/图片水印(支持平铺) |
|
|
23
|
+
|
|
24
|
+
## 快速开始
|
|
25
|
+
|
|
26
|
+
### 方式一:魔搭 MCP 广场(推荐)
|
|
27
|
+
|
|
28
|
+
在魔搭 MCP 广场搜索 **NX MCP Server**,一键同步到 Cherry Studio 或复制配置到其他客户端。
|
|
29
|
+
|
|
30
|
+
### 方式二:npx 一键运行
|
|
31
|
+
|
|
32
|
+
无需安装,客户端配置中填入:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"nx-mcp": {
|
|
38
|
+
"command": "npx",
|
|
39
|
+
"args": ["-y", "nx-mcp-server"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
如果要用智能生图,传入 API Key:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"nx-mcp": {
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["-y", "nx-mcp-server"],
|
|
53
|
+
"env": {
|
|
54
|
+
"DASHSCOPE_API_KEY": "sk-你的key"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 方式三:源码安装
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone git@gitee.com:san_jing/nx-mcp-server.git
|
|
65
|
+
cd nx-mcp-server
|
|
66
|
+
npm install
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
客户端配置:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"mcpServers": {
|
|
74
|
+
"nx-mcp": {
|
|
75
|
+
"command": "node",
|
|
76
|
+
"args": ["/你的路径/nx-mcp-server/src/index.js"],
|
|
77
|
+
"env": {
|
|
78
|
+
"DASHSCOPE_API_KEY": "sk-你的key"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## API Key
|
|
86
|
+
|
|
87
|
+
智能生图(`generate_smart`)需要通义万相 API Key,四种传入方式按优先级排序:
|
|
88
|
+
|
|
89
|
+
1. **调用时传参**:使用 `generate_smart` 时传入 `apiKey` 参数
|
|
90
|
+
2. **MCP 客户端 env**:配置中设置 `DASHSCOPE_API_KEY`(见上方示例)
|
|
91
|
+
3. **系统环境变量**:`export DASHSCOPE_API_KEY="sk-xxx"`
|
|
92
|
+
4. **config.json**:项目根目录 `config.json` 中配置 `dashscope_api_key`
|
|
93
|
+
|
|
94
|
+
> 其余 8 个本地工具无需 API Key,可直接使用。
|
|
95
|
+
|
|
96
|
+
## 传输模式
|
|
97
|
+
|
|
98
|
+
| 模式 | 环境变量 | 适用场景 |
|
|
99
|
+
|------|---------|---------|
|
|
100
|
+
| **Stdio**(默认) | 不设 | 本地 Claude Code、Cherry Studio |
|
|
101
|
+
| **HTTP** | `MCP_TRANSPORT=http` | 魔搭云端托管、远程访问 |
|
|
102
|
+
|
|
103
|
+
## 使用示例
|
|
104
|
+
|
|
105
|
+
### 智能生图
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
用 generate_smart 生成一张 prompt 为 "夕阳下的海边灯塔,水彩风格" 的图片
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 普通生图
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
用 generate_basic 生成一张 800×400 深蓝色背景、中间白色文字 "Hello World" 的 banner
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 图片拼接
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
把这三张图片用 stitch_images 拼成 2 列的网格
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 取色 + 水印
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
先 extract_colors 提取这张图的 5 个主色,再用 add_watermark 加文字水印,颜色用提取的第一个颜色
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 支持的客户端
|
|
130
|
+
|
|
131
|
+
- Cherry Studio(魔搭一键同步)
|
|
132
|
+
- Claude Desktop / Claude Code
|
|
133
|
+
- Cursor / VS Code
|
|
134
|
+
- 通义灵码
|
|
135
|
+
- 任何支持 MCP 协议的客户端
|
|
136
|
+
|
|
137
|
+
## 项目结构
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
src/
|
|
141
|
+
├── index.js # MCP Server 主入口
|
|
142
|
+
├── providers/
|
|
143
|
+
│ └── tongyi.js # 通义万相 API
|
|
144
|
+
├── tools/
|
|
145
|
+
│ ├── smart.js # 智能生图
|
|
146
|
+
│ ├── basic.js # 普通生图
|
|
147
|
+
│ ├── qrcode.js # 二维码
|
|
148
|
+
│ ├── barcode.js # 条形码
|
|
149
|
+
│ ├── compress.js # 图片压缩
|
|
150
|
+
│ ├── convert.js # 格式转换
|
|
151
|
+
│ ├── stitch.js # 图片拼接
|
|
152
|
+
│ ├── colors.js # 取色
|
|
153
|
+
│ └── watermark.js # 水印
|
|
154
|
+
└── utils/
|
|
155
|
+
├── output.js # 文件输出
|
|
156
|
+
└── gradient.js # 渐变背景
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## 依赖
|
|
160
|
+
|
|
161
|
+
- `@modelcontextprotocol/sdk` — MCP 协议
|
|
162
|
+
- `sharp` — 高性能图片处理
|
|
163
|
+
- `qrcode` — 二维码生成
|
|
164
|
+
- `axios` — HTTP 请求
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
package/config.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nx-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "图像生成 MCP Server — 智能生图(万相API) + 本地图形工具集",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nx-mcp": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"start:http": "cross-env MCP_TRANSPORT=http MCP_PORT=3000 node src/index.js",
|
|
13
|
+
"dev": "node --watch src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
17
|
+
"sharp": "^0.33.0",
|
|
18
|
+
"qrcode": "^1.5.3",
|
|
19
|
+
"axios": "^1.7.0",
|
|
20
|
+
"express": "^4.19.0"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": ["mcp", "image-generation", "qrcode", "watermark", "image-processing"],
|
|
26
|
+
"license": "MIT"
|
|
27
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import {Server} from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {CallToolRequestSchema, ListToolsRequestSchema} from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import {generateSmart} from './tools/smart.js';
|
|
5
|
+
import {generateBasic} from './tools/basic.js';
|
|
6
|
+
import {generateQrcode} from './tools/qrcode.js';
|
|
7
|
+
import {generateBarcode} from './tools/barcode.js';
|
|
8
|
+
import {compressImage} from './tools/compress.js';
|
|
9
|
+
import {convertFormat} from './tools/convert.js';
|
|
10
|
+
import {stitchImages} from './tools/stitch.js';
|
|
11
|
+
import {extractColors} from './tools/colors.js';
|
|
12
|
+
import {addWatermark} from './tools/watermark.js';
|
|
13
|
+
|
|
14
|
+
// ===== 工具注册表 =====
|
|
15
|
+
const tools=[
|
|
16
|
+
{
|
|
17
|
+
name:'generate_smart',
|
|
18
|
+
description:'智能生图:调用通义万相 API 文生图,自动下载保存到本地 output 目录',
|
|
19
|
+
inputSchema:{
|
|
20
|
+
type:'object',
|
|
21
|
+
properties:{
|
|
22
|
+
apiKey:{type:'string',description:'通义千问 API Key(可选,未传则读取环境变量 DASHSCOPE_API_KEY)'},
|
|
23
|
+
prompt:{type:'string',description:'图像描述提示词'},
|
|
24
|
+
negativePrompt:{type:'string',description:'负面提示词'},
|
|
25
|
+
width:{type:'integer',description:'宽度(px)',default:1024},
|
|
26
|
+
height:{type:'integer',description:'高度(px)',default:1024},
|
|
27
|
+
style:{type:'string',description:'风格预设',enum:['photorealistic','anime','oil-painting','watercolor','3d-render','sketch','flat-illustration']},
|
|
28
|
+
model:{type:'string',description:'模型名称',default:'wanx-v1'},
|
|
29
|
+
numImages:{type:'integer',description:'生成数量',default:1,minimum:1,maximum:4},
|
|
30
|
+
outputFormat:{type:'string',description:'输出格式',enum:['url','base64','file'],default:'url'},
|
|
31
|
+
outputDir:{type:'string',description:'输出目录'},
|
|
32
|
+
},
|
|
33
|
+
required:['prompt'],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name:'generate_basic',
|
|
38
|
+
description:'普通生图:本地 Canvas 合成图形',
|
|
39
|
+
inputSchema:{
|
|
40
|
+
type:'object',
|
|
41
|
+
properties:{
|
|
42
|
+
width:{type:'integer',description:'画布宽度(px)',default:800},
|
|
43
|
+
height:{type:'integer',description:'画布高度(px)',default:600},
|
|
44
|
+
background:{type:'string',description:'背景:纯色(hex)、渐变 JSON、transparent',default:'#ffffff'},
|
|
45
|
+
shapes:{type:'array',description:'图形列表',items:{type:'object',properties:{
|
|
46
|
+
type:{type:'string',enum:['rect','circle','ellipse','line','polygon']},
|
|
47
|
+
x:{type:'number'},y:{type:'number'},w:{type:'number'},h:{type:'number'},
|
|
48
|
+
r:{type:'number',description:'圆/椭圆半径或rx'},
|
|
49
|
+
ry:{type:'number',description:'椭圆ry'},
|
|
50
|
+
points:{type:'array',description:'多边形/线段端点',items:{type:'object',properties:{x:{type:'number'},y:{type:'number'}},required:['x','y']}},
|
|
51
|
+
fill:{type:'string',description:'填充色'},
|
|
52
|
+
stroke:{type:'string',description:'描边色'},
|
|
53
|
+
strokeWidth:{type:'number'},
|
|
54
|
+
},required:['type']}},
|
|
55
|
+
texts:{type:'array',description:'文字列表',items:{type:'object',properties:{
|
|
56
|
+
content:{type:'string'},x:{type:'number'},y:{type:'number'},
|
|
57
|
+
fontSize:{type:'number',default:24},color:{type:'string',default:'#000000'},
|
|
58
|
+
fontFamily:{type:'string',default:'sans-serif'},align:{type:'string',enum:['left','center','right']},
|
|
59
|
+
maxWidth:{type:'number'},
|
|
60
|
+
},required:['content','x','y']}},
|
|
61
|
+
images:{type:'array',description:'叠加图片',items:{type:'object',properties:{
|
|
62
|
+
src:{type:'string',description:'图片路径/URL'},x:{type:'number'},y:{type:'number'},
|
|
63
|
+
width:{type:'number'},height:{type:'number'},opacity:{type:'number',minimum:0,maximum:1},
|
|
64
|
+
},required:['src']}},
|
|
65
|
+
border:{type:'object',properties:{width:{type:'number'},color:{type:'string'},radius:{type:'number'}}},
|
|
66
|
+
format:{type:'string',enum:['png','jpeg','webp'],default:'png'},
|
|
67
|
+
quality:{type:'integer',minimum:1,maximum:100,default:90},
|
|
68
|
+
outputDir:{type:'string',description:'输出目录'},
|
|
69
|
+
},
|
|
70
|
+
required:['width','height'],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name:'generate_qrcode',
|
|
75
|
+
description:'生成二维码(支持中心logo+文字水印,一站式美化)',
|
|
76
|
+
inputSchema:{
|
|
77
|
+
type:'object',
|
|
78
|
+
properties:{
|
|
79
|
+
content:{type:'string',description:'编码内容'},
|
|
80
|
+
width:{type:'integer',description:'尺寸(px)',default:256},
|
|
81
|
+
margin:{type:'integer',description:'边距',default:4},
|
|
82
|
+
darkColor:{type:'string',default:'#000000'},
|
|
83
|
+
lightColor:{type:'string',default:'#ffffff'},
|
|
84
|
+
errorLevel:{type:'string',enum:['L','M','Q','H'],default:'M'},
|
|
85
|
+
logo:{type:'string',description:'中心logo图片路径(可选,自动加白色圆角背景遮挡QR图案)'},
|
|
86
|
+
watermark:{type:'object',description:'文字水印(可选)',properties:{
|
|
87
|
+
text:{type:'string',description:'水印文字'},
|
|
88
|
+
position:{type:'string',enum:['top','bottom'],default:'bottom'},
|
|
89
|
+
fontSize:{type:'integer',default:14},
|
|
90
|
+
color:{type:'string',description:'文字颜色'},
|
|
91
|
+
opacity:{type:'number',minimum:0,maximum:1,default:0.4},
|
|
92
|
+
}},
|
|
93
|
+
outputDir:{type:'string'},
|
|
94
|
+
},
|
|
95
|
+
required:['content'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name:'generate_barcode',
|
|
100
|
+
description:'生成条形码',
|
|
101
|
+
inputSchema:{
|
|
102
|
+
type:'object',
|
|
103
|
+
properties:{
|
|
104
|
+
content:{type:'string',description:'编码内容(数字/字母)'},
|
|
105
|
+
format:{type:'string',enum:['CODE128','CODE39','EAN13','EAN8','UPC','ITF'],default:'CODE128'},
|
|
106
|
+
width:{type:'integer',description:'宽度(px)',default:400},
|
|
107
|
+
height:{type:'integer',description:'高度(px)',default:100},
|
|
108
|
+
displayValue:{type:'boolean',description:'是否显示编码文字',default:true},
|
|
109
|
+
fontSize:{type:'integer',default:16},
|
|
110
|
+
outputDir:{type:'string'},
|
|
111
|
+
},
|
|
112
|
+
required:['content'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name:'compress_image',
|
|
117
|
+
description:'压缩图片(质量/尺寸)',
|
|
118
|
+
inputSchema:{
|
|
119
|
+
type:'object',
|
|
120
|
+
properties:{
|
|
121
|
+
src:{type:'string',description:'源图片路径/URL'},
|
|
122
|
+
quality:{type:'integer',minimum:1,maximum:100,description:'JPEG/WebP质量',default:80},
|
|
123
|
+
maxWidth:{type:'integer',description:'最大宽度(等比缩放)'},
|
|
124
|
+
maxHeight:{type:'integer',description:'最大高度(等比缩放)'},
|
|
125
|
+
format:{type:'string',enum:['jpeg','webp','png'],description:'输出格式',default:'jpeg'},
|
|
126
|
+
outputDir:{type:'string'},
|
|
127
|
+
},
|
|
128
|
+
required:['src'],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name:'convert_format',
|
|
133
|
+
description:'图片格式互转(PNG↔JPEG↔WebP↔BMP↔TIFF)',
|
|
134
|
+
inputSchema:{
|
|
135
|
+
type:'object',
|
|
136
|
+
properties:{
|
|
137
|
+
src:{type:'string',description:'源图片路径/URL'},
|
|
138
|
+
format:{type:'string',enum:['png','jpeg','webp','bmp','tiff','avif'],description:'目标格式'},
|
|
139
|
+
quality:{type:'integer',minimum:1,maximum:100,default:90},
|
|
140
|
+
background:{type:'string',description:'透明图转不透明格式时的背景色',default:'#ffffff'},
|
|
141
|
+
outputDir:{type:'string'},
|
|
142
|
+
},
|
|
143
|
+
required:['src','format'],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name:'stitch_images',
|
|
148
|
+
description:'多图拼接/网格布局',
|
|
149
|
+
inputSchema:{
|
|
150
|
+
type:'object',
|
|
151
|
+
properties:{
|
|
152
|
+
sources:{type:'array',description:'图片路径列表',items:{type:'string'}},
|
|
153
|
+
columns:{type:'integer',description:'列数',default:2},
|
|
154
|
+
gap:{type:'integer',description:'间距(px)',default:0},
|
|
155
|
+
background:{type:'string',default:'#ffffff'},
|
|
156
|
+
align:{type:'string',enum:['top','center','bottom'],default:'center'},
|
|
157
|
+
format:{type:'string',enum:['png','jpeg','webp'],default:'png'},
|
|
158
|
+
outputDir:{type:'string'},
|
|
159
|
+
},
|
|
160
|
+
required:['sources'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name:'extract_colors',
|
|
165
|
+
description:'提取图片主色调/调色板',
|
|
166
|
+
inputSchema:{
|
|
167
|
+
type:'object',
|
|
168
|
+
properties:{
|
|
169
|
+
src:{type:'string',description:'图片路径/URL'},
|
|
170
|
+
count:{type:'integer',description:'提取颜色数量',default:5,minimum:2,maximum:20},
|
|
171
|
+
format:{type:'string',enum:['hex','rgb','hsl'],default:'hex'},
|
|
172
|
+
},
|
|
173
|
+
required:['src'],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name:'add_watermark',
|
|
178
|
+
description:'添加文字/图片水印',
|
|
179
|
+
inputSchema:{
|
|
180
|
+
type:'object',
|
|
181
|
+
properties:{
|
|
182
|
+
src:{type:'string',description:'源图片路径/URL'},
|
|
183
|
+
type:{type:'string',enum:['text','image'],description:'水印类型'},
|
|
184
|
+
content:{type:'string',description:'文字水印内容'},
|
|
185
|
+
imageSrc:{type:'string',description:'图片水印路径'},
|
|
186
|
+
position:{type:'string',enum:['topLeft','topRight','bottomLeft','bottomRight','center','tile'],default:'bottomRight'},
|
|
187
|
+
margin:{type:'integer',default:20},
|
|
188
|
+
opacity:{type:'number',minimum:0,maximum:1,default:0.5},
|
|
189
|
+
fontSize:{type:'integer',default:36},
|
|
190
|
+
color:{type:'string',default:'#ffffff'},
|
|
191
|
+
fontSize:{type:'integer',default:36},
|
|
192
|
+
angle:{type:'integer',description:'旋转角度(仅tile模式)',default:-30},
|
|
193
|
+
outputDir:{type:'string'},
|
|
194
|
+
},
|
|
195
|
+
required:['src','type'],
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
// ===== 工具路由 =====
|
|
201
|
+
const handlers={
|
|
202
|
+
generate_smart:generateSmart,
|
|
203
|
+
generate_basic:generateBasic,
|
|
204
|
+
generate_qrcode:generateQrcode,
|
|
205
|
+
generate_barcode:generateBarcode,
|
|
206
|
+
compress_image:compressImage,
|
|
207
|
+
convert_format:convertFormat,
|
|
208
|
+
stitch_images:stitchImages,
|
|
209
|
+
extract_colors:extractColors,
|
|
210
|
+
add_watermark:addWatermark,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// ===== 创建 MCP Server =====
|
|
214
|
+
function createServer(){
|
|
215
|
+
const server=new Server(
|
|
216
|
+
{name:'nx-mcp-server',version:'1.0.0'},
|
|
217
|
+
{capabilities:{tools:{}}},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
server.setRequestHandler(ListToolsRequestSchema,async()=>{
|
|
221
|
+
return {tools};
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
server.setRequestHandler(CallToolRequestSchema,async(request)=>{
|
|
225
|
+
const {name,arguments:args}=request.params;
|
|
226
|
+
const handler=handlers[name];
|
|
227
|
+
if(!handler){
|
|
228
|
+
return {content:[{type:'text',text:`Unknown tool: ${name}`}],isError:true};
|
|
229
|
+
}
|
|
230
|
+
try{
|
|
231
|
+
const result=await handler(args);
|
|
232
|
+
return {
|
|
233
|
+
content:[{type:'text',text:JSON.stringify(result,null,2)}],
|
|
234
|
+
};
|
|
235
|
+
}catch(err){
|
|
236
|
+
return {
|
|
237
|
+
content:[{type:'text',text:`Error: ${err.message}`}],
|
|
238
|
+
isError:true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return server;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ===== HTTP 模式(魔塔 Remote / 远程访问) =====
|
|
247
|
+
async function startHttp(){
|
|
248
|
+
const {StreamableHTTPServerTransport}=await import('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
249
|
+
const {createMcpExpressApp}=await import('@modelcontextprotocol/sdk/server/express.js');
|
|
250
|
+
const {randomUUID}=await import('node:crypto');
|
|
251
|
+
|
|
252
|
+
const PORT=parseInt(process.env.MCP_PORT||'3000',10);
|
|
253
|
+
const app=createMcpExpressApp({host:'0.0.0.0'});
|
|
254
|
+
const transports={};// sessionId → transport
|
|
255
|
+
|
|
256
|
+
const mcpPostHandler=async (req,res)=>{
|
|
257
|
+
const sessionId=req.headers['mcp-session-id'];
|
|
258
|
+
let transport;
|
|
259
|
+
|
|
260
|
+
if(sessionId&&transports[sessionId]){
|
|
261
|
+
transport=transports[sessionId];
|
|
262
|
+
}else if(!sessionId){
|
|
263
|
+
// 新会话初始化
|
|
264
|
+
const eventStore={
|
|
265
|
+
events:[],
|
|
266
|
+
getLastEventId:()=>eventStore.events.length.toString(),
|
|
267
|
+
storeEvent:(ev)=>{eventStore.events.push(ev);},
|
|
268
|
+
getEventsAfter:(id)=>eventStore.events.slice(parseInt(id)||0),
|
|
269
|
+
};
|
|
270
|
+
transport=new StreamableHTTPServerTransport({
|
|
271
|
+
sessionIdGenerator:()=>randomUUID(),
|
|
272
|
+
eventStore,
|
|
273
|
+
onsessioninitialized:(sid)=>{
|
|
274
|
+
transports[sid]=transport;
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
transport.onclose=()=>{
|
|
278
|
+
const sid=transport.sessionId;
|
|
279
|
+
if(sid) delete transports[sid];
|
|
280
|
+
};
|
|
281
|
+
const server=createServer();
|
|
282
|
+
await server.connect(transport);
|
|
283
|
+
await transport.handleRequest(req,res,req.body);
|
|
284
|
+
return;
|
|
285
|
+
}else{
|
|
286
|
+
res.status(400).json({jsonrpc:'2.0',error:{code:-32000,message:'无效 Session ID'},id:null});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
await transport.handleRequest(req,res,req.body);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const mcpGetHandler=async (req,res)=>{
|
|
293
|
+
const sessionId=req.headers['mcp-session-id'];
|
|
294
|
+
if(!sessionId||!transports[sessionId]){
|
|
295
|
+
res.status(400).send('无效或缺少 session ID');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
await transports[sessionId].handleRequest(req,res);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const mcpDeleteHandler=async (req,res)=>{
|
|
302
|
+
const sessionId=req.headers['mcp-session-id'];
|
|
303
|
+
if(!sessionId||!transports[sessionId]){
|
|
304
|
+
res.status(400).send('无效或缺少 session ID');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
await transports[sessionId].handleRequest(req,res);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
app.post('/mcp',mcpPostHandler);
|
|
311
|
+
app.get('/mcp',mcpGetHandler);
|
|
312
|
+
app.delete('/mcp',mcpDeleteHandler);
|
|
313
|
+
|
|
314
|
+
// 健康检查
|
|
315
|
+
app.get('/health',(_req,res)=>{res.json({status:'ok',uptime:process.uptime()});});
|
|
316
|
+
|
|
317
|
+
app.listen(PORT,()=>{
|
|
318
|
+
console.error(`NX MCP Server (HTTP) → http://0.0.0.0:${PORT}/mcp`);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ===== Stdio 模式(本地 Claude Code) =====
|
|
323
|
+
async function startStdio(){
|
|
324
|
+
const server=createServer();
|
|
325
|
+
const transport=new StdioServerTransport();
|
|
326
|
+
await server.connect(transport);
|
|
327
|
+
console.error('NX MCP Server (Stdio) 已启动');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ===== 入口:根据环境变量选择传输模式 =====
|
|
331
|
+
const transportMode=process.env.MCP_TRANSPORT||'stdio';
|
|
332
|
+
|
|
333
|
+
if(transportMode==='http'){
|
|
334
|
+
startHttp().catch((err)=>{
|
|
335
|
+
console.error('HTTP 启动失败:',err);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
});
|
|
338
|
+
}else{
|
|
339
|
+
startStdio().catch((err)=>{
|
|
340
|
+
console.error('启动失败:',err);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
});
|
|
343
|
+
}
|