nx-mcp-server 1.0.8 → 1.0.9

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/README.md CHANGED
@@ -173,6 +173,41 @@ MCP_TRANSPORT=http MCP_PORT=3000 NX_API_KEY=your-key node src/index.js
173
173
 
174
174
  ---
175
175
 
176
+ ## 错误响应
177
+
178
+ 失败时返回 `isError: true`,`content[0].text` 为 JSON 字符串:
179
+
180
+ ```json
181
+ {
182
+ "error": "未配置 API Key",
183
+ "code": "MISSING_API_KEY",
184
+ "help": "请设置环境变量 NX_API_KEY 或传入 apiKey 参数…",
185
+ "doc": ""
186
+ }
187
+ ```
188
+
189
+ | 字段 | 类型 | 说明 |
190
+ |------|------|------|
191
+ | `error` | `string` | 错误描述 |
192
+ | `code` | `string` | 错误码,用于客户端程序判断(如 `MISSING_API_KEY`、`NETWORK_ERROR`、`REQUEST_TIMEOUT`) |
193
+ | `help` | `string` | 建议/帮助信息,可直接展示给用户 |
194
+ | `doc` | `string` | 文档链接(可选) |
195
+
196
+ **常见错误码:**
197
+
198
+ | code | 说明 |
199
+ |------|------|
200
+ | `MISSING_API_KEY` | 未配置 API Key |
201
+ | `MISSING_PARAMS` | 缺少 urls/files 参数 |
202
+ | `FILE_NOT_FOUND` | 本地文件不存在 |
203
+ | `DOWNLOAD_FAILED` | 远程图片下载失败 |
204
+ | `NETWORK_ERROR` | 网络请求失败 |
205
+ | `REQUEST_TIMEOUT` | 压缩请求超时 |
206
+ | `API_AUTH_FAILED` | API Key 无效 |
207
+ | `API_MISSING_AUTH` | 缺少认证头 |
208
+
209
+ ---
210
+
176
211
  ## 注意事项
177
212
 
178
213
  1. **远程 MCP 只能传 HTTP(S) 链接**,先上传到 CDN 再调用
@@ -190,6 +225,7 @@ MCP_TRANSPORT=http MCP_PORT=3000 NX_API_KEY=your-key node src/index.js
190
225
  src/
191
226
  ├── index.js # MCP Server 主入口(Stdio/HTTP 双模式)
192
227
  ├── config.js # 配置加载(config.json + 环境变量)
228
+ ├── errors.js # 统一错误类(NxError:结构化 code/help/doc)
193
229
  ├── nx-api.js # 压缩 API 客户端(JSON/FormData 双模式 + 本地文件读取)
194
230
  └── tools/
195
231
  ├── definitions.js # MCP 工具定义
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "nx-mcp-server",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "NX MCP Server — 远程图片压缩 MCP 工具",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
8
- "nx-mcp": "./src/index.js"
8
+ "nx-mcp": "src/index.js"
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node src/index.js",
package/src/config.js CHANGED
@@ -29,10 +29,6 @@ function getConfig(){
29
29
  ||'https://ai.nxtici.com';
30
30
  const apiKey=process.env.NX_API_KEY||'';
31
31
 
32
- if(!apiKey){
33
- console.error('⚠ 未配置 API Key,请设置环境变量 NX_API_KEY。获取 Key 请联系微信 zhjian_2026');
34
- }
35
-
36
32
  _config={
37
33
  api:{baseUrl:apiBaseUrl,apiKey},
38
34
  };
package/src/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ // MCP 统一错误类 —— 附带结构化字段,客户端可解析展示
2
+
3
+ class NxError extends Error{
4
+ constructor(message,{code,help,doc}={}){
5
+ super(message);
6
+ this.name='NxError';
7
+ this.code=code||'UNKNOWN';
8
+ this.help=help||'';
9
+ this.doc=doc||'';
10
+ }
11
+
12
+ // 序列化为 MCP content text(JSON 字符串)
13
+ toMcpText(){
14
+ const payload={
15
+ error:this.message,
16
+ code:this.code,
17
+ help:this.help,
18
+ doc:this.doc,
19
+ };
20
+ return JSON.stringify(payload);
21
+ }
22
+ }
23
+
24
+ export {NxError};
package/src/index.js CHANGED
@@ -28,8 +28,10 @@ function createServer(){
28
28
  content:[{type:'text',text:JSON.stringify(result,null,2)}],
29
29
  };
30
30
  }catch(err){
31
+ // 结构化错误(NxError)→ JSON;普通 Error → 纯文本,兼容旧格式
32
+ const text=err.toMcpText?err.toMcpText():`错误: ${err.message}`;
31
33
  return {
32
- content:[{type:'text',text:`错误: ${err.message}`}],
34
+ content:[{type:'text',text}],
33
35
  isError:true,
34
36
  };
35
37
  }
@@ -109,7 +111,7 @@ async function startHttp(){
109
111
  app.delete('/mcp',mcpDeleteHandler);
110
112
 
111
113
  // 全局错误处理 — 捕获 async handler 的异常
112
- app.use((err,_req,res)=>{
114
+ app.use((err,_req,res,_next)=>{
113
115
  console.error('[MCP HTTP Error]',err.message);
114
116
  res.status(500).json({jsonrpc:'2.0',error:{code:-32603,message:err.message||'Internal error'},id:null});
115
117
  });
package/src/nx-api.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {getConfig} from './config.js';
2
+ import {NxError} from './errors.js';
2
3
  import {readFileSync,existsSync,writeFileSync,mkdirSync} from 'fs';
3
4
  import {basename,dirname,join} from 'path';
4
5
 
@@ -11,7 +12,7 @@ function isFilePath(str){
11
12
  async function loadImageBuffer(src){
12
13
  // 本地文件路径
13
14
  if(isFilePath(src)){
14
- if(!existsSync(src)) throw new Error(`文件不存在: ${src}`);
15
+ if(!existsSync(src)) throw new NxError(`文件不存在: ${src}`,{code:'FILE_NOT_FOUND'});
15
16
  const buf=readFileSync(src);
16
17
  const ext=basename(src).split('.').pop()||'png';
17
18
  const mime=ext==='jpg'?'jpeg':ext;
@@ -20,7 +21,7 @@ async function loadImageBuffer(src){
20
21
  // data URL
21
22
  if(src.startsWith('data:')){
22
23
  const [head,base64]=src.split(',');
23
- if(!base64) throw new Error('data URL 格式无效');
24
+ if(!base64) throw new NxError('data URL 格式无效',{code:'INVALID_DATA_URL'});
24
25
  const mime=head.match(/:(.*?);/)?.[1]||'image/png';
25
26
  const ext=(mime.split('/')[1]||'png').replace('jpeg','jpg');
26
27
  const buf=Buffer.from(base64,'base64');
@@ -28,7 +29,7 @@ async function loadImageBuffer(src){
28
29
  }
29
30
  // HTTP(S) URL
30
31
  const res=await fetch(src);
31
- if(!res.ok) throw new Error(`下载失败 HTTP ${res.status}`);
32
+ if(!res.ok) throw new NxError(`下载失败 HTTP ${res.status}`,{code:'DOWNLOAD_FAILED'});
32
33
  const buf=Buffer.from(await res.arrayBuffer());
33
34
  const ext=src.split('.').pop().split('?')[0]||'png';
34
35
  const contentType=res.headers.get('content-type')||'';
@@ -40,7 +41,7 @@ async function loadImageBuffer(src){
40
41
  // 下载 CDN 压缩图并写入本地
41
42
  async function downloadToFile(cdnUrl,localPath){
42
43
  const res=await fetch(cdnUrl);
43
- if(!res.ok) throw new Error(`下载 CDN 失败 HTTP ${res.status}`);
44
+ if(!res.ok) throw new NxError(`下载 CDN 失败 HTTP ${res.status}`,{code:'CDN_DOWNLOAD_FAILED'});
44
45
  const buf=Buffer.from(await res.arrayBuffer());
45
46
  const dir=dirname(localPath);
46
47
  if(!existsSync(dir)) mkdirSync(dir,{recursive:true});
@@ -65,12 +66,15 @@ async function compressImage({urls=[],files=[],quality=90,output=null,apiKey=nul
65
66
  // API Key:参数传入 > 环境变量 NX_API_KEY
66
67
  const key=apiKey||api.apiKey;
67
68
  if(!key){
68
- throw new Error('未配置 API Key,请设置环境变量 NX_API_KEY 或传入 apiKey 参数');
69
+ throw new NxError('未配置 API Key',{
70
+ code:'MISSING_API_KEY',
71
+ help:'请设置环境变量 NX_API_KEY 或传入 apiKey 参数。获取 Key 请联系微信 zhjian_2026。',
72
+ });
69
73
  }
70
74
 
71
75
  // 合并所有来源,保持顺序和原始引用
72
76
  const sources=[...urls,...files];
73
- if(!sources.length) throw new Error('请提供 urls 或 files 参数,至少需要一个');
77
+ if(!sources.length) throw new NxError('请提供 urls 或 files 参数,至少需要一个',{code:'MISSING_PARAMS'});
74
78
 
75
79
  // 判断请求模式:全是 HTTP URL → JSON 模式;否则 → FormData 模式
76
80
  const allHttp=sources.every(u=>isHttpUrl(u));
@@ -100,14 +104,15 @@ async function compressImage({urls=[],files=[],quality=90,output=null,apiKey=nul
100
104
  const {buffer,name,mime}=await loadImageBuffer(src);
101
105
  return {blob:new Blob([buffer],{type:mime}),name,src};
102
106
  }catch(e){
103
- throw new Error(`加载 ${src} 失败: ${e.message}`);
107
+ throw new NxError(`加载图片失败: ${e.message}`,{code:'LOAD_IMAGE_FAILED'});
104
108
  }
105
109
  }));
106
110
  loaded.forEach((r)=>{
107
111
  if(r.status==='fulfilled'){
108
112
  fd.append('files',r.value.blob,r.value.name);
109
113
  }else{
110
- throw new Error(r.reason.message);
114
+ // 保留原始 NxError 结构,普通 Error 则包装
115
+ throw r.reason instanceof NxError?r.reason:new NxError(r.reason.message,{code:'LOAD_IMAGE_FAILED'});
111
116
  }
112
117
  });
113
118
  fd.append('quality',String(quality));
@@ -121,9 +126,9 @@ async function compressImage({urls=[],files=[],quality=90,output=null,apiKey=nul
121
126
  }catch(err){
122
127
  clearTimeout(timer);
123
128
  if(err.name==='AbortError'){
124
- throw new Error('压缩请求超时 (180s),图片可能过大,建议换小图重试');
129
+ throw new NxError('压缩请求超时 (180s)',{code:'REQUEST_TIMEOUT',help:'图片可能过大,建议换小图重试'});
125
130
  }
126
- throw new Error(`网络请求失败: ${err.message}`);
131
+ throw new NxError('网络请求失败',{code:'NETWORK_ERROR',help:err.message});
127
132
  }
128
133
  clearTimeout(timer);
129
134
 
@@ -138,7 +143,7 @@ async function compressImage({urls=[],files=[],quality=90,output=null,apiKey=nul
138
143
  missing_params:'缺少 urls 或 files 参数',
139
144
  };
140
145
  const hint=errMap[errType]||data.message||'未知错误';
141
- throw new Error(`压缩API错误 [${errType}]: ${hint}`);
146
+ throw new NxError(hint,{code:`API_${errType.toUpperCase()}`});
142
147
  }
143
148
 
144
149
  const rawItems=(data.data&&data.data.items)||[];