nx-mcp-server 1.0.4 → 1.0.6

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/src/index.js CHANGED
@@ -1,232 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import {Server} from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import {CallToolRequestSchema, ListToolsRequestSchema, isInitializeRequest} from '@modelcontextprotocol/sdk/types.js';
5
- import {generateSmart} from './tools/smart.js';
6
- import {generateBasic} from './tools/basic.js';
7
- import {generateQrcode} from './tools/qrcode.js';
8
- import {generateBarcode} from './tools/barcode.js';
9
- import {compressImage} from './tools/compress.js';
10
- import {convertFormat} from './tools/convert.js';
11
- import {stitchImages} from './tools/stitch.js';
12
- import {extractColors} from './tools/colors.js';
13
- import {addWatermark} from './tools/watermark.js';
14
-
15
- // ===== 工具注册表 =====
16
- const tools=[
17
- {
18
- name:'generate_smart',
19
- description:'智能生图:调用通义万相 API 文生图,自动下载保存到本地 output 目录',
20
- inputSchema:{
21
- type:'object',
22
- properties:{
23
- apiKey:{type:'string',description:'通义千问 API Key(可选,未传则读取环境变量 DASHSCOPE_API_KEY)'},
24
- prompt:{type:'string',description:'图像描述提示词'},
25
- negativePrompt:{type:'string',description:'负面提示词'},
26
- width:{type:'integer',description:'宽度(px)',default:1024},
27
- height:{type:'integer',description:'高度(px)',default:1024},
28
- style:{type:'string',description:'风格预设',enum:['photorealistic','anime','oil-painting','watercolor','3d-render','sketch','flat-illustration']},
29
- model:{type:'string',description:'模型名称',default:'wanx-v1'},
30
- numImages:{type:'integer',description:'生成数量',default:1,minimum:1,maximum:4},
31
- outputFormat:{type:'string',description:'输出格式',enum:['url','base64','file'],default:'url'},
32
- outputDir:{type:'string',description:'输出目录'},
33
- },
34
- required:['prompt'],
35
- },
36
- },
37
- {
38
- name:'generate_basic',
39
- description:'普通生图:本地 Canvas 合成图形',
40
- inputSchema:{
41
- type:'object',
42
- properties:{
43
- width:{type:'integer',description:'画布宽度(px)',default:800},
44
- height:{type:'integer',description:'画布高度(px)',default:600},
45
- background:{type:'string',description:'背景:纯色(hex)、渐变 JSON、transparent',default:'#ffffff'},
46
- shapes:{type:'array',description:'图形列表',items:{type:'object',properties:{
47
- type:{type:'string',enum:['rect','circle','ellipse','line','polygon']},
48
- x:{type:'number'},y:{type:'number'},w:{type:'number'},h:{type:'number'},
49
- r:{type:'number',description:'圆/椭圆半径或rx'},
50
- ry:{type:'number',description:'椭圆ry'},
51
- points:{type:'array',description:'多边形/线段端点',items:{type:'object',properties:{x:{type:'number'},y:{type:'number'}},required:['x','y']}},
52
- fill:{type:'string',description:'填充色'},
53
- stroke:{type:'string',description:'描边色'},
54
- strokeWidth:{type:'number'},
55
- },required:['type']}},
56
- texts:{type:'array',description:'文字列表',items:{type:'object',properties:{
57
- content:{type:'string'},x:{type:'number'},y:{type:'number'},
58
- fontSize:{type:'number',default:24},color:{type:'string',default:'#000000'},
59
- fontFamily:{type:'string',default:'sans-serif'},align:{type:'string',enum:['left','center','right']},
60
- maxWidth:{type:'number'},
61
- },required:['content','x','y']}},
62
- images:{type:'array',description:'叠加图片',items:{type:'object',properties:{
63
- src:{type:'string',description:'图片路径/URL'},x:{type:'number'},y:{type:'number'},
64
- width:{type:'number'},height:{type:'number'},opacity:{type:'number',minimum:0,maximum:1},
65
- },required:['src']}},
66
- border:{type:'object',properties:{width:{type:'number'},color:{type:'string'},radius:{type:'number'}}},
67
- format:{type:'string',enum:['png','jpeg','webp'],default:'png'},
68
- quality:{type:'integer',minimum:1,maximum:100,default:90},
69
- outputDir:{type:'string',description:'输出目录'},
70
- },
71
- required:['width','height'],
72
- },
73
- },
74
- {
75
- name:'generate_qrcode',
76
- description:'生成二维码(支持中心logo+文字水印,一站式美化)',
77
- inputSchema:{
78
- type:'object',
79
- properties:{
80
- content:{type:'string',description:'编码内容'},
81
- width:{type:'integer',description:'尺寸(px)',default:256},
82
- margin:{type:'integer',description:'边距',default:4},
83
- darkColor:{type:'string',default:'#000000'},
84
- lightColor:{type:'string',default:'#ffffff'},
85
- errorLevel:{type:'string',enum:['L','M','Q','H'],default:'M'},
86
- logo:{type:'string',description:'中心logo图片路径(可选,自动加白色圆角背景遮挡QR图案)'},
87
- watermark:{type:'object',description:'文字水印(可选)',properties:{
88
- text:{type:'string',description:'水印文字'},
89
- position:{type:'string',enum:['top','bottom'],default:'bottom'},
90
- fontSize:{type:'integer',default:14},
91
- color:{type:'string',description:'文字颜色'},
92
- opacity:{type:'number',minimum:0,maximum:1,default:0.4},
93
- }},
94
- outputDir:{type:'string'},
95
- },
96
- required:['content'],
97
- },
98
- },
99
- {
100
- name:'generate_barcode',
101
- description:'生成条形码',
102
- inputSchema:{
103
- type:'object',
104
- properties:{
105
- content:{type:'string',description:'编码内容(数字/字母)'},
106
- format:{type:'string',enum:['CODE128','CODE39','EAN13','EAN8','UPC','ITF'],default:'CODE128'},
107
- width:{type:'integer',description:'宽度(px)',default:400},
108
- height:{type:'integer',description:'高度(px)',default:100},
109
- displayValue:{type:'boolean',description:'是否显示编码文字',default:true},
110
- fontSize:{type:'integer',default:16},
111
- outputDir:{type:'string'},
112
- },
113
- required:['content'],
114
- },
115
- },
116
- {
117
- name:'compress_image',
118
- description:'压缩图片(质量/尺寸)',
119
- inputSchema:{
120
- type:'object',
121
- properties:{
122
- src:{type:'string',description:'源图片路径/URL'},
123
- quality:{type:'integer',minimum:1,maximum:100,description:'JPEG/WebP质量',default:80},
124
- maxWidth:{type:'integer',description:'最大宽度(等比缩放)'},
125
- maxHeight:{type:'integer',description:'最大高度(等比缩放)'},
126
- format:{type:'string',enum:['jpeg','webp','png'],description:'输出格式',default:'jpeg'},
127
- outputDir:{type:'string'},
128
- },
129
- required:['src'],
130
- },
131
- },
132
- {
133
- name:'convert_format',
134
- description:'图片格式互转(PNG↔JPEG↔WebP↔BMP↔TIFF)',
135
- inputSchema:{
136
- type:'object',
137
- properties:{
138
- src:{type:'string',description:'源图片路径/URL'},
139
- format:{type:'string',enum:['png','jpeg','webp','bmp','tiff','avif'],description:'目标格式'},
140
- quality:{type:'integer',minimum:1,maximum:100,default:90},
141
- background:{type:'string',description:'透明图转不透明格式时的背景色',default:'#ffffff'},
142
- outputDir:{type:'string'},
143
- },
144
- required:['src','format'],
145
- },
146
- },
147
- {
148
- name:'stitch_images',
149
- description:'多图拼接/网格布局',
150
- inputSchema:{
151
- type:'object',
152
- properties:{
153
- sources:{type:'array',description:'图片路径列表',items:{type:'string'}},
154
- columns:{type:'integer',description:'列数',default:2},
155
- gap:{type:'integer',description:'间距(px)',default:0},
156
- background:{type:'string',default:'#ffffff'},
157
- align:{type:'string',enum:['top','center','bottom'],default:'center'},
158
- format:{type:'string',enum:['png','jpeg','webp'],default:'png'},
159
- outputDir:{type:'string'},
160
- },
161
- required:['sources'],
162
- },
163
- },
164
- {
165
- name:'extract_colors',
166
- description:'提取图片主色调/调色板',
167
- inputSchema:{
168
- type:'object',
169
- properties:{
170
- src:{type:'string',description:'图片路径/URL'},
171
- count:{type:'integer',description:'提取颜色数量',default:5,minimum:2,maximum:20},
172
- format:{type:'string',enum:['hex','rgb','hsl'],default:'hex'},
173
- },
174
- required:['src'],
175
- },
176
- },
177
- {
178
- name:'add_watermark',
179
- description:'添加文字/图片水印',
180
- inputSchema:{
181
- type:'object',
182
- properties:{
183
- src:{type:'string',description:'源图片路径/URL'},
184
- type:{type:'string',enum:['text','image'],description:'水印类型'},
185
- content:{type:'string',description:'文字水印内容'},
186
- imageSrc:{type:'string',description:'图片水印路径'},
187
- position:{type:'string',enum:['topLeft','topRight','bottomLeft','bottomRight','center','tile'],default:'bottomRight'},
188
- margin:{type:'integer',default:20},
189
- opacity:{type:'number',minimum:0,maximum:1,default:0.5},
190
- fontSize:{type:'integer',default:36},
191
- color:{type:'string',default:'#ffffff'},
192
- fontSize:{type:'integer',default:36},
193
- angle:{type:'integer',description:'旋转角度(仅tile模式)',default:-30},
194
- outputDir:{type:'string'},
195
- },
196
- required:['src','type'],
197
- },
198
- },
199
- ];
200
-
201
- // ===== 工具路由 =====
202
- const handlers={
203
- generate_smart:generateSmart,
204
- generate_basic:generateBasic,
205
- generate_qrcode:generateQrcode,
206
- generate_barcode:generateBarcode,
207
- compress_image:compressImage,
208
- convert_format:convertFormat,
209
- stitch_images:stitchImages,
210
- extract_colors:extractColors,
211
- add_watermark:addWatermark,
212
- };
4
+ import {CallToolRequestSchema,ListToolsRequestSchema,isInitializeRequest} from '@modelcontextprotocol/sdk/types.js';
5
+ import {definitions} from './tools/definitions.js';
6
+ import {handlers} from './tools/handlers.js';
213
7
 
214
8
  // ===== 创建 MCP Server =====
215
9
  function createServer(){
216
10
  const server=new Server(
217
- {name:'nx-mcp-server',version:'1.0.0'},
11
+ {name:'nx-mcp-server',version:'1.0.5'},
218
12
  {capabilities:{tools:{}}},
219
13
  );
220
14
 
221
15
  server.setRequestHandler(ListToolsRequestSchema,async()=>{
222
- return {tools};
16
+ return {tools:definitions};
223
17
  });
224
18
 
225
19
  server.setRequestHandler(CallToolRequestSchema,async(request)=>{
226
20
  const {name,arguments:args}=request.params;
227
21
  const handler=handlers[name];
228
22
  if(!handler){
229
- return {content:[{type:'text',text:`Unknown tool: ${name}`}],isError:true};
23
+ return {content:[{type:'text',text:`未知工具: ${name}`}],isError:true};
230
24
  }
231
25
  try{
232
26
  const result=await handler(args);
@@ -235,7 +29,7 @@ function createServer(){
235
29
  };
236
30
  }catch(err){
237
31
  return {
238
- content:[{type:'text',text:`Error: ${err.message}`}],
32
+ content:[{type:'text',text:`错误: ${err.message}`}],
239
33
  isError:true,
240
34
  };
241
35
  }
@@ -252,7 +46,7 @@ async function startHttp(){
252
46
 
253
47
  const PORT=parseInt(process.env.MCP_PORT||'3000',10);
254
48
  const app=createMcpExpressApp({host:'0.0.0.0'});
255
- const transports={};// sessionId → transport
49
+ const transports={};
256
50
 
257
51
  const mcpPostHandler=async (req,res)=>{
258
52
  const sessionId=req.headers['mcp-session-id'];
@@ -261,7 +55,6 @@ async function startHttp(){
261
55
  if(sessionId&&transports[sessionId]){
262
56
  transport=transports[sessionId];
263
57
  }else if(!sessionId&&isInitializeRequest(req.body)){
264
- // 新会话初始化
265
58
  const eventStore={
266
59
  events:[],
267
60
  getLastEventId:()=>eventStore.events.length.toString(),
@@ -312,7 +105,6 @@ async function startHttp(){
312
105
  app.get('/mcp',mcpGetHandler);
313
106
  app.delete('/mcp',mcpDeleteHandler);
314
107
 
315
- // 健康检查
316
108
  app.get('/health',(_req,res)=>{res.json({status:'ok',uptime:process.uptime()});});
317
109
 
318
110
  app.listen(PORT,()=>{
@@ -320,7 +112,7 @@ async function startHttp(){
320
112
  });
321
113
  }
322
114
 
323
- // ===== Stdio 模式(本地 Claude Code) =====
115
+ // ===== Stdio 模式(本地 Claude Code / Cherry Studio) =====
324
116
  async function startStdio(){
325
117
  const server=createServer();
326
118
  const transport=new StdioServerTransport();
package/src/nx-api.js ADDED
@@ -0,0 +1,209 @@
1
+ import {getConfig} from './config.js';
2
+ import {readFileSync,existsSync,writeFileSync,mkdirSync} from 'fs';
3
+ import {basename,dirname,join} from 'path';
4
+
5
+ // 判断是否为本地文件路径
6
+ function isFilePath(str){
7
+ return /^[A-Za-z]:[\\/]/.test(str)||str.startsWith('/');
8
+ }
9
+
10
+ // 从不同来源加载图片 Buffer
11
+ async function loadImageBuffer(src){
12
+ // 本地文件路径
13
+ if(isFilePath(src)){
14
+ if(!existsSync(src)) throw new Error(`文件不存在: ${src}`);
15
+ const buf=readFileSync(src);
16
+ const ext=basename(src).split('.').pop()||'png';
17
+ const mime=ext==='jpg'?'jpeg':ext;
18
+ return {buffer:buf,name:basename(src),mime:`image/${mime}`};
19
+ }
20
+ // data URL
21
+ if(src.startsWith('data:')){
22
+ const [head,base64]=src.split(',');
23
+ if(!base64) throw new Error('data URL 格式无效');
24
+ const mime=head.match(/:(.*?);/)?.[1]||'image/png';
25
+ const ext=(mime.split('/')[1]||'png').replace('jpeg','jpg');
26
+ const buf=Buffer.from(base64,'base64');
27
+ return {buffer:buf,name:`image.${ext}`,mime};
28
+ }
29
+ // HTTP(S) URL
30
+ const res=await fetch(src);
31
+ if(!res.ok) throw new Error(`下载失败 HTTP ${res.status}`);
32
+ const buf=Buffer.from(await res.arrayBuffer());
33
+ const ext=src.split('.').pop().split('?')[0]||'png';
34
+ const contentType=res.headers.get('content-type')||'';
35
+ const mime=contentType||`image/${ext==='jpg'?'jpeg':ext}`;
36
+ const name=src.split('/').pop().split('?')[0]||`image.${ext}`;
37
+ return {buffer:buf,name,mime};
38
+ }
39
+
40
+ // 下载 CDN 压缩图并写入本地
41
+ async function downloadToFile(cdnUrl,localPath){
42
+ const res=await fetch(cdnUrl);
43
+ if(!res.ok) throw new Error(`下载 CDN 失败 HTTP ${res.status}`);
44
+ const buf=Buffer.from(await res.arrayBuffer());
45
+ const dir=dirname(localPath);
46
+ if(!existsSync(dir)) mkdirSync(dir,{recursive:true});
47
+ writeFileSync(localPath,buf);
48
+ return buf.length;
49
+ }
50
+
51
+ // ===== 图片压缩 API 客户端 =====
52
+ // 接口文档: md/compressImage-api.md
53
+ // POST /v1/nx/compressImage
54
+ // 两种请求模式:
55
+ // 模式A(JSON):纯 HTTP(S) urls → Content-Type: application/json → body: {urls,quality}
56
+ // 模式B(FormData):本地文件 / dataUrl / files → multipart/form-data → body: FormData{files,quality}
57
+
58
+ function isHttpUrl(str){
59
+ return str&&(str.startsWith('http://')||str.startsWith('https://'));
60
+ }
61
+
62
+ async function compressImage({urls=[],files=[],quality=90,output=null,apiKey=null}){
63
+ const {api}=getConfig();
64
+
65
+ // API Key:参数传入 > 环境变量 NX_API_KEY
66
+ const key=apiKey||api.apiKey;
67
+ if(!key){
68
+ throw new Error('未配置 API Key,请设置环境变量 NX_API_KEY 或传入 apiKey 参数');
69
+ }
70
+
71
+ // 合并所有来源,保持顺序和原始引用
72
+ const sources=[...urls,...files];
73
+ if(!sources.length) throw new Error('请提供 urls 或 files 参数,至少需要一个');
74
+
75
+ // 判断请求模式:全是 HTTP URL → JSON 模式;否则 → FormData 模式
76
+ const allHttp=sources.every(u=>isHttpUrl(u));
77
+ const useJson=allHttp;
78
+ if(!allHttp){
79
+ const hasLocal=sources.some(u=>isFilePath(u));
80
+ if(hasLocal) console.warn('💡 检测到本地文件路径,仅本地 MCP Server 可用。远程部署请传 HTTP(S) 图片链接。');
81
+ }
82
+
83
+ const apiUrl=`${api.baseUrl}/v1/nx/compressImage`;
84
+ const controller=new AbortController();
85
+ const timer=setTimeout(()=>controller.abort(),180000);
86
+
87
+ let body,headers;
88
+ if(useJson){
89
+ // ===== 模式A:JSON 请求(纯 HTTP URL) =====
90
+ headers={
91
+ 'Content-Type':'application/json',
92
+ 'Authorization':`Bearer ${key}`,
93
+ };
94
+ body=JSON.stringify({urls:sources,quality});
95
+ }else{
96
+ // ===== 模式B:FormData 请求(本地文件/dataUrl/files) =====
97
+ const fd=new FormData();
98
+ const loaded=await Promise.allSettled(sources.map(async (src)=>{
99
+ try{
100
+ const {buffer,name,mime}=await loadImageBuffer(src);
101
+ return {blob:new Blob([buffer],{type:mime}),name,src};
102
+ }catch(e){
103
+ throw new Error(`加载 ${src} 失败: ${e.message}`);
104
+ }
105
+ }));
106
+ loaded.forEach((r)=>{
107
+ if(r.status==='fulfilled'){
108
+ fd.append('files',r.value.blob,r.value.name);
109
+ }else{
110
+ throw new Error(r.reason.message);
111
+ }
112
+ });
113
+ fd.append('quality',String(quality));
114
+ headers={'Authorization':`Bearer ${key}`};
115
+ body=fd;
116
+ }
117
+
118
+ let res;
119
+ try{
120
+ res=await fetch(apiUrl,{method:'POST',headers,body,signal:controller.signal});
121
+ }catch(err){
122
+ clearTimeout(timer);
123
+ if(err.name==='AbortError'){
124
+ throw new Error('压缩请求超时 (180s),图片可能过大,建议换小图重试');
125
+ }
126
+ throw new Error(`网络请求失败: ${err.message}`);
127
+ }
128
+ clearTimeout(timer);
129
+
130
+ const data=await res.json();
131
+
132
+ if(data.code!==0){
133
+ const errType=data.error_type||'unknown';
134
+ const errMap={
135
+ missing_auth:'未提供 Authorization 头',
136
+ auth_failed:'API Key 无效',
137
+ auth_exception:'认证服务异常,请重试',
138
+ missing_params:'缺少 urls 或 files 参数',
139
+ };
140
+ const hint=errMap[errType]||data.message||'未知错误';
141
+ throw new Error(`压缩API错误 [${errType}]: ${hint}`);
142
+ }
143
+
144
+ const rawItems=(data.data&&data.data.items)||[];
145
+ const successCount=rawItems.filter(it=>!it.error).length;
146
+ const failCount=rawItems.filter(it=>it.error).length;
147
+
148
+ // 构建 items,originalUrl 保留原始输入
149
+ const items=rawItems.map((it,i)=>({
150
+ originalUrl:sources[i]||it.original_url,
151
+ compressedUrl:it.compressed_url||null,
152
+ originalSize:it.original_size,
153
+ compressedSize:it.compressed_size,
154
+ ratio:it.ratio||'0%',
155
+ error:it.error||null,
156
+ }));
157
+
158
+ // ===== 处理 output =====
159
+ let outputResult=null;
160
+ if(output&&items.length){
161
+ const compressed=items.filter(it=>it.compressedUrl);
162
+ if(output==='overwrite'){
163
+ let overwritten=0;
164
+ for(const it of compressed){
165
+ if(isFilePath(it.originalUrl)){
166
+ try{
167
+ const size=await downloadToFile(it.compressedUrl,it.originalUrl);
168
+ it.outputPath=it.originalUrl;
169
+ it.outputSize=size;
170
+ overwritten++;
171
+ }catch(e){
172
+ it.outputError=e.message;
173
+ }
174
+ }
175
+ }
176
+ outputResult={mode:'overwrite',count:overwritten};
177
+ }else{
178
+ let saved=0;
179
+ for(const it of compressed){
180
+ try{
181
+ const name=basename(it.originalUrl)||`compressed_${Date.now()}`;
182
+ const outPath=join(output,name);
183
+ const size=await downloadToFile(it.compressedUrl,outPath);
184
+ it.outputPath=outPath;
185
+ it.outputSize=size;
186
+ saved++;
187
+ }catch(e){
188
+ it.outputError=e.message;
189
+ }
190
+ }
191
+ outputResult={mode:'save',dir:output,count:saved};
192
+ }
193
+ }
194
+
195
+ return {
196
+ success:failCount===0,
197
+ message:data.message||`压缩完成: 成功 ${successCount}, 失败 ${failCount}`,
198
+ mode:useJson?'json':'formdata',
199
+ items,
200
+ summary:{
201
+ total:items.length,
202
+ success:successCount,
203
+ failed:failCount,
204
+ },
205
+ ...(outputResult&&{output:outputResult}),
206
+ };
207
+ }
208
+
209
+ export {compressImage};
@@ -0,0 +1,19 @@
1
+ // MCP 工具定义
2
+
3
+ const definitions=[
4
+ {
5
+ name:'nx_compress',
6
+ description:'远程智能压缩图片。两种方式:①传 urls(HTTP链接或本地路径);②传 files(dataUrl 格式)。支持 PNG/JPEG/BMP/WebP/TGA。支持 output 参数直接写入本地文件',
7
+ inputSchema:{
8
+ type:'object',
9
+ properties:{
10
+ urls:{type:'array',description:'[方式一] 图片URL列表,支持 HTTP(S) 链接、本地路径、data URL。远程调用请传 HTTP(S) 链接',items:{type:'string'}},
11
+ files:{type:'array',description:'[方式二] 图片 dataUrl 列表(data:image/...;base64,...)。仅 FormData 模式,适合本地大图',items:{type:'string'}},
12
+ quality:{type:'integer',description:'压缩质量 1-100,数值越大质量越高',minimum:1,maximum:100,default:90},
13
+ output:{type:'string',description:'输出方式:"overwrite" 覆盖原文件(仅本地路径有效),或指定输出目录路径如 E:/output/'},
14
+ },
15
+ },
16
+ },
17
+ ];
18
+
19
+ export {definitions};
@@ -0,0 +1,15 @@
1
+ import {compressImage} from '../nx-api.js';
2
+
3
+ // ===== 工具处理函数 =====
4
+
5
+ async function handleCompress(args){
6
+ return compressImage({urls:args.urls,files:args.files,quality:args.quality,output:args.output});
7
+ }
8
+
9
+ // ===== 工具路由表 =====
10
+
11
+ const handlers={
12
+ nx_compress:handleCompress,
13
+ };
14
+
15
+ export {handlers};
@@ -1,11 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "nx-mcp": {
4
- "command": "node",
5
- "args": ["src/index.js"],
6
- "env": {
7
- "DASHSCOPE_API_KEY": ""
8
- }
9
- }
10
- }
11
- }