@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 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
@@ -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,9 @@
1
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare const uploadLocalFileTool: Tool;
3
+ export declare function handleUpload(args: {
4
+ file_path: string;
5
+ folder?: string;
6
+ }): Promise<{
7
+ content: any[];
8
+ isError?: boolean;
9
+ }>;
@@ -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,5 @@
1
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare const serverInfoTool: Tool;
3
+ export declare function handleServerInfo(): Promise<{
4
+ content: any[];
5
+ }>;
@@ -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,6 @@
1
+ export declare class FileReader {
2
+ validate(filePath: string): Promise<void>;
3
+ readAsBase64(filePath: string): Promise<string>;
4
+ getFileName(filePath: string): string;
5
+ getFileSize(filePath: string): number;
6
+ }
@@ -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
+ }