@raiseinfo/smartorder-stdio-mcp 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/README.md +152 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +43 -0
- package/dist/tools/file-upload.d.ts +9 -0
- package/dist/tools/file-upload.js +107 -0
- package/dist/tools/server-info.d.ts +5 -0
- package/dist/tools/server-info.js +32 -0
- package/dist/types/index.d.ts +41 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/cache.d.ts +10 -0
- package/dist/utils/cache.js +46 -0
- package/dist/utils/file-reader.d.ts +6 -0
- package/dist/utils/file-reader.js +35 -0
- package/dist/utils/http-client.d.ts +10 -0
- package/dist/utils/http-client.js +81 -0
- package/package.json +24 -0
- package/src/index.ts +49 -0
- package/src/tools/file-upload.ts +115 -0
- package/src/tools/server-info.ts +37 -0
- package/src/types/index.ts +46 -0
- package/src/utils/cache.ts +55 -0
- package/src/utils/file-reader.ts +42 -0
- package/src/utils/http-client.ts +95 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @raiseinfo/smartorder-stdio-mcp
|
|
2
|
+
|
|
3
|
+
智单通文件上传 MCP Server (stdio 模式)
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- **upload_local_file**: 读取本地文件并上传到智单通 OSS 存储
|
|
8
|
+
- **get_mcp_server_info**: 获取服务状态和配置信息
|
|
9
|
+
- 代理上传模式,不存储任何 OSS 密钥
|
|
10
|
+
- 本地缓存机制,8小时自动刷新
|
|
11
|
+
- **npx 自动升级**:每次启动自动获取最新版本
|
|
12
|
+
|
|
13
|
+
## 支持的文件类型
|
|
14
|
+
|
|
15
|
+
图片:jpg, jpeg, png, gif, webp
|
|
16
|
+
文档:pdf, xlsx, xls, doc, docx
|
|
17
|
+
最大文件大小:10MB
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
### 方式一:npx(推荐,自动升级)
|
|
22
|
+
|
|
23
|
+
无需安装,Agent 客户端直接使用 npx 运行:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @raiseinfo/smartorder-stdio-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 方式二:全局安装
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g @raiseinfo/smartorder-stdio-mcp
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 配置
|
|
36
|
+
|
|
37
|
+
### 环境变量
|
|
38
|
+
|
|
39
|
+
| 变量 | 说明 | 示例 | 必填 |
|
|
40
|
+
|------|------|------|------|
|
|
41
|
+
| TENANT_CODE | 租户编码 | TC1773503403286M2IQ63TB | ✅ |
|
|
42
|
+
| MCP_TOKEN | MCP访问令牌 | MCP710710AEDF1A4C51850B990F524BFEA5 | ✅ |
|
|
43
|
+
| SMARTORDER_API_BASE | API基础地址(可选) | https://smart-order.raiseinfo.cn | ❌ |
|
|
44
|
+
| SMARTORDER_CACHE_DIR | 缓存目录(可选) | ~/.smartorder-stdio/ | ❌ |
|
|
45
|
+
|
|
46
|
+
## 使用
|
|
47
|
+
|
|
48
|
+
### Wordbobby / OpenClaw 配置
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"smartorder-file": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": ["@raiseinfo/smartorder-stdio-mcp"],
|
|
56
|
+
"env": {
|
|
57
|
+
"TENANT_CODE": "你的租户编码",
|
|
58
|
+
"MCP_TOKEN": "你的MCP令牌"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Claude Desktop 配置
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"smartorder-file": {
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": ["@raiseinfo/smartorder-stdio-mcp"],
|
|
73
|
+
"env": {
|
|
74
|
+
"TENANT_CODE": "${TENANT_CODE}",
|
|
75
|
+
"MCP_TOKEN": "${MCP_TOKEN}"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## npx 自动升级机制
|
|
83
|
+
|
|
84
|
+
采用 **npx** 方案实现自动升级,每次启动 Agent 时自动获取最新版本:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
88
|
+
│ npx 工作流程 │
|
|
89
|
+
└──────────────────────────────────────────────────────────────┘
|
|
90
|
+
|
|
91
|
+
1. Agent 启动 stdio MCP Server
|
|
92
|
+
|
|
93
|
+
2. npx 检查本地缓存是否过期
|
|
94
|
+
|
|
95
|
+
3. 如果有新版本:
|
|
96
|
+
- 首次运行:下载最新版本到缓存
|
|
97
|
+
- 后续运行:直接使用缓存(快速启动)
|
|
98
|
+
|
|
99
|
+
4. 如果是最新版本:
|
|
100
|
+
- 直接使用缓存运行
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**优势**:
|
|
104
|
+
- 无需手动升级,发布新版本后自动生效
|
|
105
|
+
- 缓存机制保证后续启动速度
|
|
106
|
+
- 无需维护更新逻辑
|
|
107
|
+
|
|
108
|
+
## 工具说明
|
|
109
|
+
|
|
110
|
+
### upload_local_file
|
|
111
|
+
|
|
112
|
+
读取本地文件并上传到智单通存储服务。
|
|
113
|
+
|
|
114
|
+
**参数:**
|
|
115
|
+
- file_path: 本地文件绝对路径(必填)
|
|
116
|
+
- folder: 存储目录(可选,默认 smart-order/)
|
|
117
|
+
|
|
118
|
+
**返回:**
|
|
119
|
+
- url: 文件访问URL
|
|
120
|
+
- md5: 文件MD5哈希值
|
|
121
|
+
- fileName: 原始文件名
|
|
122
|
+
|
|
123
|
+
### get_mcp_server_info
|
|
124
|
+
|
|
125
|
+
获取智单通MCP文件上传服务的基本信息。
|
|
126
|
+
|
|
127
|
+
**返回:**
|
|
128
|
+
- name: 服务名称
|
|
129
|
+
- version: 版本号
|
|
130
|
+
- supportedTypes: 支持的文件类型
|
|
131
|
+
- maxFileSize: 最大文件大小
|
|
132
|
+
- apiBase: API基础地址
|
|
133
|
+
|
|
134
|
+
## 开发
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# 安装依赖
|
|
138
|
+
npm install
|
|
139
|
+
|
|
140
|
+
# 开发模式
|
|
141
|
+
npm run dev
|
|
142
|
+
|
|
143
|
+
# 构建
|
|
144
|
+
npm run build
|
|
145
|
+
|
|
146
|
+
# 测试
|
|
147
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 许可
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { uploadLocalFileTool, handleUpload } from './tools/file-upload.js';
|
|
6
|
+
import { serverInfoTool, handleServerInfo } from './tools/server-info.js';
|
|
7
|
+
const server = new Server({
|
|
8
|
+
name: 'smartorder-stdio-mcp',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
}, {
|
|
11
|
+
capabilities: {
|
|
12
|
+
tools: {},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
16
|
+
return {
|
|
17
|
+
tools: [uploadLocalFileTool, serverInfoTool],
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
21
|
+
const { name, arguments: args } = request.params;
|
|
22
|
+
try {
|
|
23
|
+
switch (name) {
|
|
24
|
+
case 'upload_local_file':
|
|
25
|
+
return await handleUpload(args);
|
|
26
|
+
case 'get_mcp_server_info':
|
|
27
|
+
return await handleServerInfo();
|
|
28
|
+
default:
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: 'text', text: `未知工具:${name}` }],
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: error.message }],
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const transport = new StdioServerTransport();
|
|
43
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { CacheManager } from '../utils/cache.js';
|
|
2
|
+
import { HttpClient } from '../utils/http-client.js';
|
|
3
|
+
import { FileReader } from '../utils/file-reader.js';
|
|
4
|
+
const cacheManager = new CacheManager();
|
|
5
|
+
const httpClient = new HttpClient();
|
|
6
|
+
const fileReader = new FileReader();
|
|
7
|
+
export const uploadLocalFileTool = {
|
|
8
|
+
name: 'upload_local_file',
|
|
9
|
+
description: `读取本地文件并上传到智单通存储服务(代理上传模式)。
|
|
10
|
+
|
|
11
|
+
【工作流程】
|
|
12
|
+
1. 验证本地文件(存在、大小、类型)
|
|
13
|
+
2. 使用缓存的API配置上传
|
|
14
|
+
3. 上传失败时根据错误码处理(404立即报错,5xx重试1次)
|
|
15
|
+
|
|
16
|
+
【安全说明】
|
|
17
|
+
- 采用代理上传模式,不存储任何OSS密钥
|
|
18
|
+
- 文件通过后端服务转存到OSS
|
|
19
|
+
|
|
20
|
+
【支持的文件类型】
|
|
21
|
+
- 图片:jpg, jpeg, png, gif, webp
|
|
22
|
+
- 文档:pdf, xlsx, xls, doc, docx
|
|
23
|
+
- 最大文件大小:10MB`,
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
file_path: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: '本地文件绝对路径,如 /Users/ken/orders/file.xlsx',
|
|
30
|
+
},
|
|
31
|
+
folder: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'OSS存储目录',
|
|
34
|
+
default: 'smart-order/',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['file_path'],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export async function handleUpload(args) {
|
|
41
|
+
const { file_path, folder = 'smart-order/' } = args;
|
|
42
|
+
await fileReader.validate(file_path);
|
|
43
|
+
let apiUrl;
|
|
44
|
+
let cache = await cacheManager.load();
|
|
45
|
+
if (cache && !cacheManager.isExpired(cache)) {
|
|
46
|
+
apiUrl = cache.apiUrl;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
try {
|
|
50
|
+
const config = await httpClient.getConfig();
|
|
51
|
+
apiUrl = config.data.apiUrl;
|
|
52
|
+
await cacheManager.save({
|
|
53
|
+
version: 1,
|
|
54
|
+
apiUrl: config.data.apiUrl,
|
|
55
|
+
folder: config.data.folder,
|
|
56
|
+
prefix: config.data.prefix,
|
|
57
|
+
tenantCode: process.env.TENANT_CODE || '',
|
|
58
|
+
expiresIn: config.data.expiresIn,
|
|
59
|
+
obtainedAt: Date.now(),
|
|
60
|
+
expiresAt: Date.now() + config.data.expiresIn * 1000,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'text', text: `获取配置失败:${error.message}` }],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const result = await httpClient.uploadFile(file_path, folder);
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: 'text',
|
|
76
|
+
text: JSON.stringify(result.data, null, 2),
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.message.includes('404')) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: 'text', text: error.message }],
|
|
85
|
+
isError: true,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (error.message.includes('5xx') || error.message.includes('网络')) {
|
|
89
|
+
try {
|
|
90
|
+
const result = await httpClient.uploadFile(file_path, folder);
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text', text: '服务暂时不可用,请稍后重试' }],
|
|
98
|
+
isError: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: error.message }],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const serverInfoTool = {
|
|
2
|
+
name: 'get_mcp_server_info',
|
|
3
|
+
description: `获取智单通MCP文件上传服务的基本信息。
|
|
4
|
+
|
|
5
|
+
【返回信息】
|
|
6
|
+
- 服务名称和版本
|
|
7
|
+
- 支持的文件类型
|
|
8
|
+
- 最大文件大小
|
|
9
|
+
- 当前连接状态
|
|
10
|
+
- 缓存状态`,
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
export async function handleServerInfo() {
|
|
17
|
+
const info = {
|
|
18
|
+
name: '智单通文件上传服务',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
supportedTypes: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'xlsx', 'xls', 'doc', 'docx'],
|
|
21
|
+
maxFileSize: '10MB',
|
|
22
|
+
apiBase: process.env.SMARTORDER_API_BASE || 'https://smart-order.raiseinfo.cn',
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: 'text',
|
|
28
|
+
text: JSON.stringify(info, null, 2),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface ApiCache {
|
|
2
|
+
version: number;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
folder: string;
|
|
5
|
+
prefix: string;
|
|
6
|
+
tenantCode: string;
|
|
7
|
+
expiresIn: number;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
obtainedAt: number;
|
|
10
|
+
}
|
|
11
|
+
export interface UploadConfig {
|
|
12
|
+
apiUrl: string;
|
|
13
|
+
folder: string;
|
|
14
|
+
prefix: string;
|
|
15
|
+
maxFileSize: number;
|
|
16
|
+
allowedTypes: string[];
|
|
17
|
+
expiresIn: number;
|
|
18
|
+
}
|
|
19
|
+
export interface UploadResult {
|
|
20
|
+
url: string;
|
|
21
|
+
fileName: string;
|
|
22
|
+
size: number;
|
|
23
|
+
md5: string;
|
|
24
|
+
}
|
|
25
|
+
export interface McpConfigResponse {
|
|
26
|
+
code: number;
|
|
27
|
+
msg?: string;
|
|
28
|
+
data: UploadConfig;
|
|
29
|
+
}
|
|
30
|
+
export interface McpUploadResponse {
|
|
31
|
+
code: number;
|
|
32
|
+
msg?: string;
|
|
33
|
+
data: UploadResult;
|
|
34
|
+
}
|
|
35
|
+
export interface ServerInfo {
|
|
36
|
+
name: string;
|
|
37
|
+
version: string;
|
|
38
|
+
supportedTypes: string[];
|
|
39
|
+
maxFileSize: string;
|
|
40
|
+
apiBase: string;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ApiCache } from '../types/index.js';
|
|
2
|
+
export declare class CacheManager {
|
|
3
|
+
private cacheDir;
|
|
4
|
+
private cacheFile;
|
|
5
|
+
constructor();
|
|
6
|
+
load(): Promise<ApiCache | null>;
|
|
7
|
+
save(cache: ApiCache): Promise<void>;
|
|
8
|
+
clear(): Promise<void>;
|
|
9
|
+
isExpired(cache: ApiCache): boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const CACHE_FILE = 'api-cache.json';
|
|
5
|
+
const CACHE_VERSION = 1;
|
|
6
|
+
export class CacheManager {
|
|
7
|
+
cacheDir;
|
|
8
|
+
cacheFile;
|
|
9
|
+
constructor() {
|
|
10
|
+
const baseDir = process.env.SMARTORDER_CACHE_DIR || path.join(os.homedir(), '.smartorder-stdio');
|
|
11
|
+
this.cacheDir = baseDir;
|
|
12
|
+
this.cacheFile = path.join(baseDir, CACHE_FILE);
|
|
13
|
+
}
|
|
14
|
+
async load() {
|
|
15
|
+
await fs.promises.mkdir(this.cacheDir, { recursive: true });
|
|
16
|
+
try {
|
|
17
|
+
const data = await fs.promises.readFile(this.cacheFile, 'utf-8');
|
|
18
|
+
const cache = JSON.parse(data);
|
|
19
|
+
if (cache.version !== CACHE_VERSION) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return cache;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async save(cache) {
|
|
29
|
+
await fs.promises.mkdir(this.cacheDir, { recursive: true });
|
|
30
|
+
cache.version = CACHE_VERSION;
|
|
31
|
+
cache.obtainedAt = Date.now();
|
|
32
|
+
cache.expiresAt = Date.now() + (cache.expiresIn || 28800) * 1000;
|
|
33
|
+
await fs.promises.writeFile(this.cacheFile, JSON.stringify(cache, null, 2));
|
|
34
|
+
}
|
|
35
|
+
async clear() {
|
|
36
|
+
try {
|
|
37
|
+
await fs.promises.unlink(this.cacheFile);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// 忽略错误
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
isExpired(cache) {
|
|
44
|
+
return Date.now() > cache.expiresAt;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const ALLOWED_TYPES = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'xlsx', 'xls', 'doc', 'docx'];
|
|
4
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
5
|
+
export class FileReader {
|
|
6
|
+
async validate(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
await fs.promises.access(filePath, fs.constants.R_OK);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error(`文件不存在:${filePath},请检查路径是否正确`);
|
|
12
|
+
}
|
|
13
|
+
const stats = await fs.promises.stat(filePath);
|
|
14
|
+
if (!stats.isFile()) {
|
|
15
|
+
throw new Error(`路径不是文件:${filePath}`);
|
|
16
|
+
}
|
|
17
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
18
|
+
throw new Error(`文件大小超过限制(最大10MB):${stats.size} bytes`);
|
|
19
|
+
}
|
|
20
|
+
const ext = path.extname(filePath).toLowerCase().replace('.', '');
|
|
21
|
+
if (!ALLOWED_TYPES.includes(ext)) {
|
|
22
|
+
throw new Error(`不支持的文件类型:.${ext},支持:${ALLOWED_TYPES.join(', ')}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async readAsBase64(filePath) {
|
|
26
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
27
|
+
return buffer.toString('base64');
|
|
28
|
+
}
|
|
29
|
+
getFileName(filePath) {
|
|
30
|
+
return path.basename(filePath);
|
|
31
|
+
}
|
|
32
|
+
getFileSize(filePath) {
|
|
33
|
+
return fs.statSync(filePath).size;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { McpConfigResponse, McpUploadResponse } from '../types/index.js';
|
|
2
|
+
export declare class HttpClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private tenantCode;
|
|
5
|
+
private mcpToken;
|
|
6
|
+
constructor();
|
|
7
|
+
getConfig(): Promise<McpConfigResponse>;
|
|
8
|
+
uploadFile(filePath: string, folder: string): Promise<McpUploadResponse>;
|
|
9
|
+
private request;
|
|
10
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import FormData from 'form-data';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import https from 'https';
|
|
6
|
+
export class HttpClient {
|
|
7
|
+
baseUrl;
|
|
8
|
+
tenantCode;
|
|
9
|
+
mcpToken;
|
|
10
|
+
constructor() {
|
|
11
|
+
this.baseUrl = process.env.SMARTORDER_API_BASE || 'https://smart-order.raiseinfo.cn';
|
|
12
|
+
this.tenantCode = process.env.TENANT_CODE || '';
|
|
13
|
+
this.mcpToken = process.env.MCP_TOKEN || '';
|
|
14
|
+
}
|
|
15
|
+
async getConfig() {
|
|
16
|
+
const url = `${this.baseUrl}/api/mcp/config`;
|
|
17
|
+
return this.request('GET', url);
|
|
18
|
+
}
|
|
19
|
+
async uploadFile(filePath, folder) {
|
|
20
|
+
const url = `${this.baseUrl}/api/mcp/upload`;
|
|
21
|
+
const form = new FormData();
|
|
22
|
+
const fileStream = fs.createReadStream(filePath);
|
|
23
|
+
const fileName = path.basename(filePath);
|
|
24
|
+
form.append('file', fileStream, { filename: fileName });
|
|
25
|
+
form.append('folder', folder);
|
|
26
|
+
return this.request('POST', url, form, true);
|
|
27
|
+
}
|
|
28
|
+
request(method, url, body, isFormData = false) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const urlObj = new URL(url);
|
|
31
|
+
const client = urlObj.protocol === 'https:' ? https : http;
|
|
32
|
+
const options = {
|
|
33
|
+
hostname: urlObj.hostname,
|
|
34
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
35
|
+
path: urlObj.pathname,
|
|
36
|
+
method,
|
|
37
|
+
headers: {
|
|
38
|
+
'X-Tenant-Code': this.tenantCode,
|
|
39
|
+
'X-Mcp-Token': this.mcpToken,
|
|
40
|
+
...(isFormData ? body.getHeaders() : { 'Content-Type': 'application/json' }),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const req = client.request(options, (res) => {
|
|
44
|
+
let data = '';
|
|
45
|
+
res.on('data', (chunk) => (data += chunk));
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
const json = JSON.parse(data);
|
|
49
|
+
if (res.statusCode === 200) {
|
|
50
|
+
resolve(json);
|
|
51
|
+
}
|
|
52
|
+
else if (res.statusCode === 401 || res.statusCode === 403) {
|
|
53
|
+
reject(new Error('认证失败,请检查租户Token配置'));
|
|
54
|
+
}
|
|
55
|
+
else if (res.statusCode === 404) {
|
|
56
|
+
reject(new Error('上传服务路径不存在,请联系管理员'));
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
reject(new Error(`服务返回错误:${res.statusCode}`));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
reject(new Error('响应解析失败'));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
req.on('error', (err) => {
|
|
68
|
+
reject(new Error(`网络错误:${err.message}`));
|
|
69
|
+
});
|
|
70
|
+
if (body) {
|
|
71
|
+
if (isFormData) {
|
|
72
|
+
body.pipe(req);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
req.write(JSON.stringify(body));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
req.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@raiseinfo/smartorder-stdio-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "智单通文件上传 MCP Server (stdio模式)",
|
|
6
|
+
"bin": {
|
|
7
|
+
"smartorder-stdio-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"postbuild": "node -e \"const fs=require('fs');const f='dist/index.js';let c=fs.readFileSync(f,'utf8');if(!c.startsWith('#!')){fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c);}\"",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
17
|
+
"form-data": "^4.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^20.0.0",
|
|
21
|
+
"tsx": "^4.0.0",
|
|
22
|
+
"typescript": "^5.0.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
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 { uploadLocalFileTool, handleUpload } from './tools/file-upload.js';
|
|
5
|
+
import { serverInfoTool, handleServerInfo } from './tools/server-info.js';
|
|
6
|
+
|
|
7
|
+
const server = new Server(
|
|
8
|
+
{
|
|
9
|
+
name: 'smartorder-stdio-mcp',
|
|
10
|
+
version: '1.0.0',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {},
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
20
|
+
return {
|
|
21
|
+
tools: [uploadLocalFileTool, serverInfoTool],
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
26
|
+
const { name, arguments: args } = request.params;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
switch (name) {
|
|
30
|
+
case 'upload_local_file':
|
|
31
|
+
return await handleUpload(args as { file_path: string; folder?: string });
|
|
32
|
+
case 'get_mcp_server_info':
|
|
33
|
+
return await handleServerInfo();
|
|
34
|
+
default:
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: `未知工具:${name}` }],
|
|
37
|
+
isError: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: error.message }],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const transport = new StdioServerTransport();
|
|
49
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { CacheManager } from '../utils/cache.js';
|
|
3
|
+
import { HttpClient } from '../utils/http-client.js';
|
|
4
|
+
import { FileReader } from '../utils/file-reader.js';
|
|
5
|
+
|
|
6
|
+
const cacheManager = new CacheManager();
|
|
7
|
+
const httpClient = new HttpClient();
|
|
8
|
+
const fileReader = new FileReader();
|
|
9
|
+
|
|
10
|
+
export const uploadLocalFileTool: Tool = {
|
|
11
|
+
name: 'upload_local_file',
|
|
12
|
+
description: `读取本地文件并上传到智单通存储服务(代理上传模式)。
|
|
13
|
+
|
|
14
|
+
【工作流程】
|
|
15
|
+
1. 验证本地文件(存在、大小、类型)
|
|
16
|
+
2. 使用缓存的API配置上传
|
|
17
|
+
3. 上传失败时根据错误码处理(404立即报错,5xx重试1次)
|
|
18
|
+
|
|
19
|
+
【安全说明】
|
|
20
|
+
- 采用代理上传模式,不存储任何OSS密钥
|
|
21
|
+
- 文件通过后端服务转存到OSS
|
|
22
|
+
|
|
23
|
+
【支持的文件类型】
|
|
24
|
+
- 图片:jpg, jpeg, png, gif, webp
|
|
25
|
+
- 文档:pdf, xlsx, xls, doc, docx
|
|
26
|
+
- 最大文件大小:10MB`,
|
|
27
|
+
|
|
28
|
+
inputSchema: {
|
|
29
|
+
type: 'object',
|
|
30
|
+
properties: {
|
|
31
|
+
file_path: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: '本地文件绝对路径,如 /Users/ken/orders/file.xlsx',
|
|
34
|
+
},
|
|
35
|
+
folder: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'OSS存储目录',
|
|
38
|
+
default: 'smart-order/',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ['file_path'],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function handleUpload(args: { file_path: string; folder?: string }): Promise<{ content: any[]; isError?: boolean }> {
|
|
46
|
+
const { file_path, folder = 'smart-order/' } = args;
|
|
47
|
+
|
|
48
|
+
await fileReader.validate(file_path);
|
|
49
|
+
|
|
50
|
+
let apiUrl: string;
|
|
51
|
+
let cache = await cacheManager.load();
|
|
52
|
+
|
|
53
|
+
if (cache && !cacheManager.isExpired(cache)) {
|
|
54
|
+
apiUrl = cache.apiUrl;
|
|
55
|
+
} else {
|
|
56
|
+
try {
|
|
57
|
+
const config = await httpClient.getConfig();
|
|
58
|
+
apiUrl = config.data.apiUrl;
|
|
59
|
+
|
|
60
|
+
await cacheManager.save({
|
|
61
|
+
version: 1,
|
|
62
|
+
apiUrl: config.data.apiUrl,
|
|
63
|
+
folder: config.data.folder,
|
|
64
|
+
prefix: config.data.prefix,
|
|
65
|
+
tenantCode: process.env.TENANT_CODE || '',
|
|
66
|
+
expiresIn: config.data.expiresIn,
|
|
67
|
+
obtainedAt: Date.now(),
|
|
68
|
+
expiresAt: Date.now() + config.data.expiresIn * 1000,
|
|
69
|
+
});
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: 'text', text: `获取配置失败:${error.message}` }],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await httpClient.uploadFile(file_path, folder);
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: JSON.stringify(result.data, null, 2),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
if (error.message.includes('404')) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: 'text', text: error.message }],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (error.message.includes('5xx') || error.message.includes('网络')) {
|
|
97
|
+
try {
|
|
98
|
+
const result = await httpClient.uploadFile(file_path, folder);
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }],
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: 'text', text: '服务暂时不可用,请稍后重试' }],
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
content: [{ type: 'text', text: error.message }],
|
|
112
|
+
isError: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
|
|
3
|
+
export const serverInfoTool: Tool = {
|
|
4
|
+
name: 'get_mcp_server_info',
|
|
5
|
+
description: `获取智单通MCP文件上传服务的基本信息。
|
|
6
|
+
|
|
7
|
+
【返回信息】
|
|
8
|
+
- 服务名称和版本
|
|
9
|
+
- 支持的文件类型
|
|
10
|
+
- 最大文件大小
|
|
11
|
+
- 当前连接状态
|
|
12
|
+
- 缓存状态`,
|
|
13
|
+
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {},
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function handleServerInfo(): Promise<{ content: any[] }> {
|
|
21
|
+
const info = {
|
|
22
|
+
name: '智单通文件上传服务',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
supportedTypes: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'xlsx', 'xls', 'doc', 'docx'],
|
|
25
|
+
maxFileSize: '10MB',
|
|
26
|
+
apiBase: process.env.SMARTORDER_API_BASE || 'https://smart-order.raiseinfo.cn',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: JSON.stringify(info, null, 2),
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface ApiCache {
|
|
2
|
+
version: number;
|
|
3
|
+
apiUrl: string;
|
|
4
|
+
folder: string;
|
|
5
|
+
prefix: string;
|
|
6
|
+
tenantCode: string;
|
|
7
|
+
expiresIn: number;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
obtainedAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UploadConfig {
|
|
13
|
+
apiUrl: string;
|
|
14
|
+
folder: string;
|
|
15
|
+
prefix: string;
|
|
16
|
+
maxFileSize: number;
|
|
17
|
+
allowedTypes: string[];
|
|
18
|
+
expiresIn: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UploadResult {
|
|
22
|
+
url: string;
|
|
23
|
+
fileName: string;
|
|
24
|
+
size: number;
|
|
25
|
+
md5: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface McpConfigResponse {
|
|
29
|
+
code: number;
|
|
30
|
+
msg?: string;
|
|
31
|
+
data: UploadConfig;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface McpUploadResponse {
|
|
35
|
+
code: number;
|
|
36
|
+
msg?: string;
|
|
37
|
+
data: UploadResult;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ServerInfo {
|
|
41
|
+
name: string;
|
|
42
|
+
version: string;
|
|
43
|
+
supportedTypes: string[];
|
|
44
|
+
maxFileSize: string;
|
|
45
|
+
apiBase: string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { ApiCache } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
const CACHE_FILE = 'api-cache.json';
|
|
7
|
+
const CACHE_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
export class CacheManager {
|
|
10
|
+
private cacheDir: string;
|
|
11
|
+
private cacheFile: string;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
const baseDir = process.env.SMARTORDER_CACHE_DIR || path.join(os.homedir(), '.smartorder-stdio');
|
|
15
|
+
this.cacheDir = baseDir;
|
|
16
|
+
this.cacheFile = path.join(baseDir, CACHE_FILE);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async load(): Promise<ApiCache | null> {
|
|
20
|
+
await fs.promises.mkdir(this.cacheDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const data = await fs.promises.readFile(this.cacheFile, 'utf-8');
|
|
24
|
+
const cache = JSON.parse(data) as ApiCache;
|
|
25
|
+
|
|
26
|
+
if (cache.version !== CACHE_VERSION) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return cache;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async save(cache: ApiCache): Promise<void> {
|
|
37
|
+
await fs.promises.mkdir(this.cacheDir, { recursive: true });
|
|
38
|
+
cache.version = CACHE_VERSION;
|
|
39
|
+
cache.obtainedAt = Date.now();
|
|
40
|
+
cache.expiresAt = Date.now() + (cache.expiresIn || 28800) * 1000;
|
|
41
|
+
await fs.promises.writeFile(this.cacheFile, JSON.stringify(cache, null, 2));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async clear(): Promise<void> {
|
|
45
|
+
try {
|
|
46
|
+
await fs.promises.unlink(this.cacheFile);
|
|
47
|
+
} catch {
|
|
48
|
+
// 忽略错误
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isExpired(cache: ApiCache): boolean {
|
|
53
|
+
return Date.now() > cache.expiresAt;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const ALLOWED_TYPES = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf', 'xlsx', 'xls', 'doc', 'docx'];
|
|
5
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export class FileReader {
|
|
8
|
+
async validate(filePath: string): Promise<void> {
|
|
9
|
+
try {
|
|
10
|
+
await fs.promises.access(filePath, fs.constants.R_OK);
|
|
11
|
+
} catch {
|
|
12
|
+
throw new Error(`文件不存在:${filePath},请检查路径是否正确`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const stats = await fs.promises.stat(filePath);
|
|
16
|
+
if (!stats.isFile()) {
|
|
17
|
+
throw new Error(`路径不是文件:${filePath}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
21
|
+
throw new Error(`文件大小超过限制(最大10MB):${stats.size} bytes`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ext = path.extname(filePath).toLowerCase().replace('.', '');
|
|
25
|
+
if (!ALLOWED_TYPES.includes(ext)) {
|
|
26
|
+
throw new Error(`不支持的文件类型:.${ext},支持:${ALLOWED_TYPES.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async readAsBase64(filePath: string): Promise<string> {
|
|
31
|
+
const buffer = await fs.promises.readFile(filePath);
|
|
32
|
+
return buffer.toString('base64');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
getFileName(filePath: string): string {
|
|
36
|
+
return path.basename(filePath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getFileSize(filePath: string): number {
|
|
40
|
+
return fs.statSync(filePath).size;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import FormData from 'form-data';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import https from 'https';
|
|
6
|
+
import { McpConfigResponse, McpUploadResponse } from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
export class HttpClient {
|
|
9
|
+
private baseUrl: string;
|
|
10
|
+
private tenantCode: string;
|
|
11
|
+
private mcpToken: string;
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
this.baseUrl = process.env.SMARTORDER_API_BASE || 'https://smart-order.raiseinfo.cn';
|
|
15
|
+
this.tenantCode = process.env.TENANT_CODE || '';
|
|
16
|
+
this.mcpToken = process.env.MCP_TOKEN || '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async getConfig(): Promise<McpConfigResponse> {
|
|
20
|
+
const url = `${this.baseUrl}/api/mcp/config`;
|
|
21
|
+
return this.request('GET', url) as Promise<McpConfigResponse>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async uploadFile(filePath: string, folder: string): Promise<McpUploadResponse> {
|
|
25
|
+
const url = `${this.baseUrl}/api/mcp/upload`;
|
|
26
|
+
const form = new FormData();
|
|
27
|
+
|
|
28
|
+
const fileStream = fs.createReadStream(filePath);
|
|
29
|
+
const fileName = path.basename(filePath);
|
|
30
|
+
|
|
31
|
+
form.append('file', fileStream, { filename: fileName });
|
|
32
|
+
form.append('folder', folder);
|
|
33
|
+
|
|
34
|
+
return this.request('POST', url, form, true) as Promise<McpUploadResponse>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private request(
|
|
38
|
+
method: string,
|
|
39
|
+
url: string,
|
|
40
|
+
body?: any,
|
|
41
|
+
isFormData: boolean = false
|
|
42
|
+
): Promise<any> {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const urlObj = new URL(url);
|
|
45
|
+
const client = urlObj.protocol === 'https:' ? https : http;
|
|
46
|
+
|
|
47
|
+
const options: http.RequestOptions = {
|
|
48
|
+
hostname: urlObj.hostname,
|
|
49
|
+
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
50
|
+
path: urlObj.pathname,
|
|
51
|
+
method,
|
|
52
|
+
headers: {
|
|
53
|
+
'X-Tenant-Code': this.tenantCode,
|
|
54
|
+
'X-Mcp-Token': this.mcpToken,
|
|
55
|
+
...(isFormData ? body.getHeaders() : { 'Content-Type': 'application/json' }),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const req = client.request(options, (res) => {
|
|
60
|
+
let data = '';
|
|
61
|
+
res.on('data', (chunk) => (data += chunk));
|
|
62
|
+
res.on('end', () => {
|
|
63
|
+
try {
|
|
64
|
+
const json = JSON.parse(data);
|
|
65
|
+
if (res.statusCode === 200) {
|
|
66
|
+
resolve(json);
|
|
67
|
+
} else if (res.statusCode === 401 || res.statusCode === 403) {
|
|
68
|
+
reject(new Error('认证失败,请检查租户Token配置'));
|
|
69
|
+
} else if (res.statusCode === 404) {
|
|
70
|
+
reject(new Error('上传服务路径不存在,请联系管理员'));
|
|
71
|
+
} else {
|
|
72
|
+
reject(new Error(`服务返回错误:${res.statusCode}`));
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
reject(new Error('响应解析失败'));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
req.on('error', (err) => {
|
|
81
|
+
reject(new Error(`网络错误:${err.message}`));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (body) {
|
|
85
|
+
if (isFormData) {
|
|
86
|
+
body.pipe(req);
|
|
87
|
+
} else {
|
|
88
|
+
req.write(JSON.stringify(body));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
req.end();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|