auth-ssh-mcp 2.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/LICENSE +21 -0
- package/README.md +162 -0
- package/bin/cli.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +797 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# auth-ssh-mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) Server for SSH credential management. Connects to [auth-ssh](https://github.com/your-username/auth-ssh) backend service.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **SSH Command Execution**: Execute commands on remote servers
|
|
8
|
+
- **SFTP Operations**: List directories, read/write files
|
|
9
|
+
- **Smart Upload**: Automatically handles large files (up to 500MB) via streaming upload
|
|
10
|
+
- **Secure**: Uses API Key authentication
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g auth-ssh-mcp
|
|
16
|
+
# or use directly with npx
|
|
17
|
+
npx auth-ssh-mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
### Environment Variables
|
|
23
|
+
|
|
24
|
+
| Variable | Required | Description |
|
|
25
|
+
|----------|----------|-------------|
|
|
26
|
+
| `MCP_SERVER_URL` | Yes | auth-ssh backend service URL |
|
|
27
|
+
| `MCP_API_KEY` | Yes | API Key (starts with `sk_`) |
|
|
28
|
+
|
|
29
|
+
### Claude Desktop Configuration
|
|
30
|
+
|
|
31
|
+
Add to your `claude_desktop_config.json`:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"auth-ssh": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["auth-ssh-mcp"],
|
|
39
|
+
"env": {
|
|
40
|
+
"MCP_SERVER_URL": "https://your-auth-ssh-server.com",
|
|
41
|
+
"MCP_API_KEY": "sk_your_api_key_here"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Claude Code Configuration
|
|
49
|
+
|
|
50
|
+
Add to your Claude Code MCP settings:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"auth-ssh": {
|
|
56
|
+
"command": "npx",
|
|
57
|
+
"args": ["auth-ssh-mcp"],
|
|
58
|
+
"env": {
|
|
59
|
+
"MCP_SERVER_URL": "https://your-auth-ssh-server.com",
|
|
60
|
+
"MCP_API_KEY": "sk_your_api_key_here"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Available Tools
|
|
68
|
+
|
|
69
|
+
### `list_credentials`
|
|
70
|
+
|
|
71
|
+
List all SSH credentials configured for the current user.
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
No parameters required
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### `ssh_exec`
|
|
78
|
+
|
|
79
|
+
Execute a command on a remote server.
|
|
80
|
+
|
|
81
|
+
| Parameter | Type | Required | Description |
|
|
82
|
+
|-----------|------|----------|-------------|
|
|
83
|
+
| `credentialId` | string (UUID) | Yes | SSH credential ID |
|
|
84
|
+
| `command` | string | Yes | Command to execute |
|
|
85
|
+
| `timeout` | number | No | Timeout in seconds (default: 60, max: 300) |
|
|
86
|
+
|
|
87
|
+
### `sftp_list`
|
|
88
|
+
|
|
89
|
+
List files and directories in a remote path.
|
|
90
|
+
|
|
91
|
+
| Parameter | Type | Required | Description |
|
|
92
|
+
|-----------|------|----------|-------------|
|
|
93
|
+
| `credentialId` | string (UUID) | Yes | SSH credential ID |
|
|
94
|
+
| `path` | string | No | Directory path (default: `~`) |
|
|
95
|
+
| `showHidden` | boolean | No | Show hidden files (default: false) |
|
|
96
|
+
|
|
97
|
+
### `sftp_read`
|
|
98
|
+
|
|
99
|
+
Read a text file from the remote server.
|
|
100
|
+
|
|
101
|
+
| Parameter | Type | Required | Description |
|
|
102
|
+
|-----------|------|----------|-------------|
|
|
103
|
+
| `credentialId` | string (UUID) | Yes | SSH credential ID |
|
|
104
|
+
| `path` | string | Yes | File path |
|
|
105
|
+
| `maxSize` | number | No | Max bytes to read (default: 1MB) |
|
|
106
|
+
|
|
107
|
+
### `sftp_write`
|
|
108
|
+
|
|
109
|
+
Write content to a file on the remote server. Automatically uses streaming upload for files larger than 1MB.
|
|
110
|
+
|
|
111
|
+
| Parameter | Type | Required | Description |
|
|
112
|
+
|-----------|------|----------|-------------|
|
|
113
|
+
| `credentialId` | string (UUID) | Yes | SSH credential ID |
|
|
114
|
+
| `path` | string | Yes | File path |
|
|
115
|
+
| `content` | string | Yes | File content |
|
|
116
|
+
| `mode` | number | No | File permissions (default: 0o644) |
|
|
117
|
+
|
|
118
|
+
**Upload Limits:**
|
|
119
|
+
- Files ≤ 1MB: Uses JSON API
|
|
120
|
+
- Files > 1MB: Uses streaming upload (up to 500MB)
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Install dependencies
|
|
126
|
+
pnpm install
|
|
127
|
+
|
|
128
|
+
# Build
|
|
129
|
+
pnpm build
|
|
130
|
+
|
|
131
|
+
# Development with watch mode
|
|
132
|
+
pnpm dev
|
|
133
|
+
|
|
134
|
+
# Type check
|
|
135
|
+
pnpm typecheck
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How It Works
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
┌─────────────────┐ stdio ┌─────────────────┐
|
|
142
|
+
│ MCP Client │ ◄────────────► │ auth-ssh-mcp │
|
|
143
|
+
│ (Claude Desktop │ │ (this CLI) │
|
|
144
|
+
│ / Claude Code) │ └────────┬────────┘
|
|
145
|
+
└─────────────────┘ │
|
|
146
|
+
│ HTTP
|
|
147
|
+
▼
|
|
148
|
+
┌─────────────────┐
|
|
149
|
+
│ auth-ssh │
|
|
150
|
+
│ (backend) │
|
|
151
|
+
└────────┬────────┘
|
|
152
|
+
│
|
|
153
|
+
│ SSH/SFTP
|
|
154
|
+
▼
|
|
155
|
+
┌─────────────────┐
|
|
156
|
+
│ Remote Servers │
|
|
157
|
+
└─────────────────┘
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
package/bin/cli.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
CallToolRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
|
|
11
|
+
// src/mcp-client.ts
|
|
12
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
13
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
14
|
+
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
|
|
16
|
+
// src/error-codes.ts
|
|
17
|
+
var ErrorCode = {
|
|
18
|
+
// 认证模块
|
|
19
|
+
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
20
|
+
AUTH_FORBIDDEN: "AUTH_FORBIDDEN",
|
|
21
|
+
AUTH_TOKEN_EXPIRED: "AUTH_TOKEN_EXPIRED",
|
|
22
|
+
// 验证模块
|
|
23
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
24
|
+
// 凭证模块
|
|
25
|
+
CREDENTIAL_NOT_FOUND: "CREDENTIAL_NOT_FOUND",
|
|
26
|
+
CREDENTIAL_CONNECTION_FAILED: "CREDENTIAL_CONNECTION_FAILED",
|
|
27
|
+
// 文件模块
|
|
28
|
+
FILE_NOT_FOUND: "FILE_NOT_FOUND",
|
|
29
|
+
FILE_ALREADY_EXISTS: "FILE_ALREADY_EXISTS",
|
|
30
|
+
FILE_PERMISSION_DENIED: "FILE_PERMISSION_DENIED",
|
|
31
|
+
// 分块上传模块
|
|
32
|
+
CHUNKED_SESSION_NOT_FOUND: "CHUNKED_SESSION_NOT_FOUND",
|
|
33
|
+
CHUNKED_SESSION_LIMIT_EXCEEDED: "CHUNKED_SESSION_LIMIT_EXCEEDED",
|
|
34
|
+
CHUNKED_INVALID_CHUNK_INDEX: "CHUNKED_INVALID_CHUNK_INDEX",
|
|
35
|
+
CHUNKED_INVALID_CHUNK_SIZE: "CHUNKED_INVALID_CHUNK_SIZE",
|
|
36
|
+
CHUNKED_INCOMPLETE_UPLOAD: "CHUNKED_INCOMPLETE_UPLOAD",
|
|
37
|
+
CHUNKED_HASH_MISMATCH: "CHUNKED_HASH_MISMATCH",
|
|
38
|
+
CHUNKED_IDLE_TIMEOUT: "CHUNKED_IDLE_TIMEOUT",
|
|
39
|
+
SFTP_WRITE_TIMEOUT: "SFTP_WRITE_TIMEOUT",
|
|
40
|
+
SFTP_CONNECTION_CLOSED: "SFTP_CONNECTION_CLOSED",
|
|
41
|
+
// 内部错误
|
|
42
|
+
INTERNAL_ERROR: "INTERNAL_ERROR"
|
|
43
|
+
};
|
|
44
|
+
var ErrorMessages = {
|
|
45
|
+
// 认证模块
|
|
46
|
+
[ErrorCode.AUTH_UNAUTHORIZED]: "\u9700\u8981 API Key \u8BA4\u8BC1",
|
|
47
|
+
[ErrorCode.AUTH_FORBIDDEN]: "\u65E0\u6743\u8BBF\u95EE\u6B64\u8D44\u6E90",
|
|
48
|
+
[ErrorCode.AUTH_TOKEN_EXPIRED]: "API Key \u5DF2\u8FC7\u671F",
|
|
49
|
+
// 验证模块
|
|
50
|
+
[ErrorCode.VALIDATION_ERROR]: "\u53C2\u6570\u9A8C\u8BC1\u5931\u8D25",
|
|
51
|
+
// 凭证模块
|
|
52
|
+
[ErrorCode.CREDENTIAL_NOT_FOUND]: "\u51ED\u8BC1\u4E0D\u5B58\u5728",
|
|
53
|
+
[ErrorCode.CREDENTIAL_CONNECTION_FAILED]: "\u65E0\u6CD5\u8FDE\u63A5\u5230\u8FDC\u7A0B\u670D\u52A1\u5668",
|
|
54
|
+
// 文件模块
|
|
55
|
+
[ErrorCode.FILE_NOT_FOUND]: "\u6587\u4EF6\u6216\u76EE\u5F55\u4E0D\u5B58\u5728",
|
|
56
|
+
[ErrorCode.FILE_ALREADY_EXISTS]: "\u6587\u4EF6\u5DF2\u5B58\u5728",
|
|
57
|
+
[ErrorCode.FILE_PERMISSION_DENIED]: "\u6CA1\u6709\u6587\u4EF6\u64CD\u4F5C\u6743\u9650",
|
|
58
|
+
// 分块上传模块
|
|
59
|
+
[ErrorCode.CHUNKED_SESSION_NOT_FOUND]: "\u4E0A\u4F20\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F",
|
|
60
|
+
[ErrorCode.CHUNKED_SESSION_LIMIT_EXCEEDED]: "\u5DF2\u8FBE\u5230\u6700\u5927\u5E76\u53D1\u4E0A\u4F20\u6570\u9650\u5236",
|
|
61
|
+
[ErrorCode.CHUNKED_INVALID_CHUNK_INDEX]: "\u65E0\u6548\u7684\u5206\u5757\u7D22\u5F15",
|
|
62
|
+
[ErrorCode.CHUNKED_INVALID_CHUNK_SIZE]: "\u65E0\u6548\u7684\u5206\u5757\u5927\u5C0F",
|
|
63
|
+
[ErrorCode.CHUNKED_INCOMPLETE_UPLOAD]: "\u4E0A\u4F20\u672A\u5B8C\u6210\uFF0C\u5B58\u5728\u7F3A\u5931\u7684\u5206\u5757",
|
|
64
|
+
[ErrorCode.CHUNKED_HASH_MISMATCH]: "\u6587\u4EF6\u5B8C\u6574\u6027\u6821\u9A8C\u5931\u8D25",
|
|
65
|
+
[ErrorCode.CHUNKED_IDLE_TIMEOUT]: "\u4E0A\u4F20\u7A7A\u95F2\u8D85\u65F6\uFF0C\u6570\u636E\u6D41\u4E2D\u65AD\u8D85\u8FC7 60 \u79D2",
|
|
66
|
+
[ErrorCode.SFTP_WRITE_TIMEOUT]: "SFTP \u5199\u5165\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5",
|
|
67
|
+
[ErrorCode.SFTP_CONNECTION_CLOSED]: "SFTP \u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u8BD5",
|
|
68
|
+
// 内部错误
|
|
69
|
+
[ErrorCode.INTERNAL_ERROR]: "\u670D\u52A1\u5668\u5185\u90E8\u9519\u8BEF"
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/error-handler.ts
|
|
73
|
+
var ErrorCategory = {
|
|
74
|
+
/** 超时类错误 - 可重试 */
|
|
75
|
+
RETRYABLE_TIMEOUT: "RETRYABLE_TIMEOUT",
|
|
76
|
+
/** 网络类错误 - 可重试 */
|
|
77
|
+
RETRYABLE_NETWORK: "RETRYABLE_NETWORK",
|
|
78
|
+
/** 服务端错误 - 可重试 */
|
|
79
|
+
RETRYABLE_SERVER: "RETRYABLE_SERVER",
|
|
80
|
+
/** 会话过期 - 需重新初始化 */
|
|
81
|
+
SESSION_EXPIRED: "SESSION_EXPIRED",
|
|
82
|
+
/** 不可重试错误 */
|
|
83
|
+
NON_RETRYABLE: "NON_RETRYABLE"
|
|
84
|
+
};
|
|
85
|
+
var ERROR_CATEGORY_MAP = {
|
|
86
|
+
// 超时类 - 可重试
|
|
87
|
+
[ErrorCode.CHUNKED_IDLE_TIMEOUT]: ErrorCategory.RETRYABLE_TIMEOUT,
|
|
88
|
+
[ErrorCode.SFTP_WRITE_TIMEOUT]: ErrorCategory.RETRYABLE_TIMEOUT,
|
|
89
|
+
// 网络类 - 可重试
|
|
90
|
+
[ErrorCode.SFTP_CONNECTION_CLOSED]: ErrorCategory.RETRYABLE_NETWORK,
|
|
91
|
+
// 会话过期 - 需重新初始化
|
|
92
|
+
[ErrorCode.CHUNKED_SESSION_NOT_FOUND]: ErrorCategory.SESSION_EXPIRED,
|
|
93
|
+
// 不可重试
|
|
94
|
+
[ErrorCode.FILE_ALREADY_EXISTS]: ErrorCategory.NON_RETRYABLE,
|
|
95
|
+
[ErrorCode.CHUNKED_SESSION_LIMIT_EXCEEDED]: ErrorCategory.NON_RETRYABLE,
|
|
96
|
+
[ErrorCode.CHUNKED_HASH_MISMATCH]: ErrorCategory.NON_RETRYABLE,
|
|
97
|
+
[ErrorCode.CREDENTIAL_NOT_FOUND]: ErrorCategory.NON_RETRYABLE,
|
|
98
|
+
[ErrorCode.VALIDATION_ERROR]: ErrorCategory.NON_RETRYABLE
|
|
99
|
+
};
|
|
100
|
+
var ApiError = class extends Error {
|
|
101
|
+
code;
|
|
102
|
+
statusCode;
|
|
103
|
+
details;
|
|
104
|
+
constructor(code, message, statusCode, details) {
|
|
105
|
+
const finalMessage = message || ErrorMessages[code] || code;
|
|
106
|
+
super(finalMessage);
|
|
107
|
+
this.name = "ApiError";
|
|
108
|
+
this.code = code;
|
|
109
|
+
this.statusCode = statusCode;
|
|
110
|
+
this.details = details;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
async function parseApiResponse(response) {
|
|
114
|
+
const contentType = response.headers.get("content-type") || "";
|
|
115
|
+
if (!contentType.includes("application/json")) {
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
throw new ApiError(
|
|
118
|
+
"INTERNAL_ERROR",
|
|
119
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
120
|
+
response.status
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
const json = await response.json();
|
|
126
|
+
if (!response.ok || typeof json === "object" && json !== null && "error" in json) {
|
|
127
|
+
const errorResponse = json;
|
|
128
|
+
const error = errorResponse.error || { code: "INTERNAL_ERROR", message: "" };
|
|
129
|
+
throw new ApiError(
|
|
130
|
+
error.code,
|
|
131
|
+
error.message || ErrorMessages[error.code],
|
|
132
|
+
response.status,
|
|
133
|
+
error.details
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (typeof json === "object" && json !== null && "data" in json) {
|
|
137
|
+
return json.data;
|
|
138
|
+
}
|
|
139
|
+
return json;
|
|
140
|
+
}
|
|
141
|
+
function formatErrorMessage(error) {
|
|
142
|
+
if (error instanceof ApiError) {
|
|
143
|
+
const parts = [error.message];
|
|
144
|
+
if (error.code && error.code !== error.message) {
|
|
145
|
+
parts.push(`[${error.code}]`);
|
|
146
|
+
}
|
|
147
|
+
if (error.statusCode) {
|
|
148
|
+
parts.push(`(HTTP ${error.statusCode})`);
|
|
149
|
+
}
|
|
150
|
+
if (error.details) {
|
|
151
|
+
parts.push(`
|
|
152
|
+
\u8BE6\u60C5: ${JSON.stringify(error.details)}`);
|
|
153
|
+
}
|
|
154
|
+
return parts.join(" ");
|
|
155
|
+
}
|
|
156
|
+
if (error instanceof Error) {
|
|
157
|
+
if (error.message.includes("ECONNREFUSED")) {
|
|
158
|
+
return "\u65E0\u6CD5\u8FDE\u63A5\u5230\u8FDC\u7A0B\u670D\u52A1\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5";
|
|
159
|
+
}
|
|
160
|
+
if (error.message.includes("ENOTFOUND")) {
|
|
161
|
+
return "\u65E0\u6CD5\u89E3\u6790\u670D\u52A1\u5668\u5730\u5740";
|
|
162
|
+
}
|
|
163
|
+
if (error.message.includes("ETIMEDOUT")) {
|
|
164
|
+
return "\u8FDE\u63A5\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
165
|
+
}
|
|
166
|
+
if (error.message.includes("ECONNRESET")) {
|
|
167
|
+
return "\u8FDE\u63A5\u88AB\u91CD\u7F6E\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5";
|
|
168
|
+
}
|
|
169
|
+
return error.message;
|
|
170
|
+
}
|
|
171
|
+
return String(error);
|
|
172
|
+
}
|
|
173
|
+
function isRetryableError(error) {
|
|
174
|
+
if (error instanceof ApiError) {
|
|
175
|
+
const category = ERROR_CATEGORY_MAP[error.code];
|
|
176
|
+
if (category === ErrorCategory.RETRYABLE_TIMEOUT || category === ErrorCategory.RETRYABLE_NETWORK || category === ErrorCategory.RETRYABLE_SERVER) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
if (error.statusCode === 408) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
if (error.statusCode && error.statusCode >= 500) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (error instanceof Error) {
|
|
187
|
+
if (error.message.includes("ECONNREFUSED") || error.message.includes("ETIMEDOUT") || error.message.includes("ECONNRESET")) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
function isSessionExpiredError(error) {
|
|
194
|
+
if (error instanceof ApiError) {
|
|
195
|
+
return ERROR_CATEGORY_MAP[error.code] === ErrorCategory.SESSION_EXPIRED;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
function calculateDelay(attempt, config) {
|
|
200
|
+
return config.baseDelay * Math.pow(config.multiplier, attempt);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/mcp-client.ts
|
|
204
|
+
var CLIENT_NAME = "auth-ssh-mcp";
|
|
205
|
+
var CLIENT_VERSION = "1.0.0";
|
|
206
|
+
var McpClient = class {
|
|
207
|
+
client;
|
|
208
|
+
transport = null;
|
|
209
|
+
serverUrl;
|
|
210
|
+
apiKey;
|
|
211
|
+
connected = false;
|
|
212
|
+
constructor(config) {
|
|
213
|
+
this.serverUrl = config.serverUrl.replace(/\/$/, "");
|
|
214
|
+
this.apiKey = config.apiKey;
|
|
215
|
+
this.client = new Client({
|
|
216
|
+
name: CLIENT_NAME,
|
|
217
|
+
version: CLIENT_VERSION
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 连接到远程 MCP Server
|
|
222
|
+
*/
|
|
223
|
+
async connect() {
|
|
224
|
+
if (this.connected) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const mcpUrl = new URL(`${this.serverUrl}/api/mcp`);
|
|
229
|
+
this.transport = new StreamableHTTPClientTransport(mcpUrl, {
|
|
230
|
+
requestInit: {
|
|
231
|
+
headers: {
|
|
232
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
await this.client.connect(this.transport);
|
|
237
|
+
this.connected = true;
|
|
238
|
+
console.error(`[${CLIENT_NAME}] \u5DF2\u8FDE\u63A5\u5230\u8FDC\u7A0B MCP Server`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
this.connected = false;
|
|
241
|
+
throw this.wrapError(error, "\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1\u5931\u8D25");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 获取远程工具列表
|
|
246
|
+
*/
|
|
247
|
+
async listTools() {
|
|
248
|
+
if (!this.connected) {
|
|
249
|
+
throw new Error("\u672A\u8FDE\u63A5\u5230\u8FDC\u7A0B\u670D\u52A1\uFF0C\u8BF7\u5148\u8C03\u7528 connect()");
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const result = await this.client.listTools();
|
|
253
|
+
console.error(`[${CLIENT_NAME}] \u83B7\u53D6\u5230 ${result.tools.length} \u4E2A\u8FDC\u7A0B\u5DE5\u5177`);
|
|
254
|
+
return result;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
throw this.wrapError(error, "\u83B7\u53D6\u5DE5\u5177\u5217\u8868\u5931\u8D25");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 调用远程工具
|
|
261
|
+
*/
|
|
262
|
+
async callTool(name, args) {
|
|
263
|
+
if (!this.connected) {
|
|
264
|
+
throw new Error("\u672A\u8FDE\u63A5\u5230\u8FDC\u7A0B\u670D\u52A1\uFF0C\u8BF7\u5148\u8C03\u7528 connect()");
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const result = await this.client.callTool({ name, arguments: args }, CallToolResultSchema);
|
|
268
|
+
return result;
|
|
269
|
+
} catch (error) {
|
|
270
|
+
throw this.wrapError(error, `\u8C03\u7528\u5DE5\u5177 ${name} \u5931\u8D25`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 关闭连接
|
|
275
|
+
*/
|
|
276
|
+
async close() {
|
|
277
|
+
if (!this.connected) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
await this.client.close();
|
|
282
|
+
this.connected = false;
|
|
283
|
+
this.transport = null;
|
|
284
|
+
console.error(`[${CLIENT_NAME}] \u5DF2\u65AD\u5F00\u8FDE\u63A5`);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error(`[${CLIENT_NAME}] \u5173\u95ED\u8FDE\u63A5\u65F6\u51FA\u9519:`, error);
|
|
287
|
+
this.connected = false;
|
|
288
|
+
this.transport = null;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 检查连接状态
|
|
293
|
+
*/
|
|
294
|
+
isConnected() {
|
|
295
|
+
return this.connected;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* 包装错误,转换为 ApiError
|
|
299
|
+
*/
|
|
300
|
+
wrapError(error, context) {
|
|
301
|
+
if (error instanceof ApiError) {
|
|
302
|
+
return error;
|
|
303
|
+
}
|
|
304
|
+
const originalMessage = error instanceof Error ? error.message : String(error);
|
|
305
|
+
if (originalMessage.includes("401") || originalMessage.includes("403") || originalMessage.includes("Unauthorized")) {
|
|
306
|
+
return new ApiError(ErrorCode.AUTH_UNAUTHORIZED, "API Key \u65E0\u6548\u6216\u5DF2\u88AB\u64A4\u9500", 401);
|
|
307
|
+
}
|
|
308
|
+
if (originalMessage.includes("ECONNREFUSED") || originalMessage.includes("ENOTFOUND") || originalMessage.includes("ETIMEDOUT")) {
|
|
309
|
+
return new ApiError(ErrorCode.CREDENTIAL_CONNECTION_FAILED, "\u65E0\u6CD5\u8FDE\u63A5\u5230\u8FDC\u7A0B\u670D\u52A1\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u548C\u670D\u52A1\u5730\u5740");
|
|
310
|
+
}
|
|
311
|
+
if (originalMessage.includes("404") || originalMessage.includes("Session not found")) {
|
|
312
|
+
return new ApiError(ErrorCode.CHUNKED_SESSION_NOT_FOUND, "\u4F1A\u8BDD\u5DF2\u8FC7\u671F", 404);
|
|
313
|
+
}
|
|
314
|
+
if (originalMessage.includes("500") || originalMessage.includes("502") || originalMessage.includes("503")) {
|
|
315
|
+
return new ApiError(ErrorCode.INTERNAL_ERROR, "\u8FDC\u7A0B\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", 500);
|
|
316
|
+
}
|
|
317
|
+
return new ApiError(ErrorCode.INTERNAL_ERROR, `${context}: ${originalMessage}`);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/chunked-upload-client.ts
|
|
322
|
+
import { createHash } from "crypto";
|
|
323
|
+
import { createReadStream, statSync } from "fs";
|
|
324
|
+
import { open } from "fs/promises";
|
|
325
|
+
var LOG_PREFIX = "[ChunkedUpload]";
|
|
326
|
+
function formatSize(bytes) {
|
|
327
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
328
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
329
|
+
return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
330
|
+
}
|
|
331
|
+
var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
332
|
+
var MIN_CHUNK_SIZE = 1 * 1024 * 1024;
|
|
333
|
+
var MAX_CHUNK_SIZE = 10 * 1024 * 1024;
|
|
334
|
+
var MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024;
|
|
335
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
336
|
+
maxRetries: 3,
|
|
337
|
+
baseDelay: 1e3,
|
|
338
|
+
multiplier: 2
|
|
339
|
+
};
|
|
340
|
+
function sleep(ms) {
|
|
341
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
342
|
+
}
|
|
343
|
+
var ChunkedUploadClient = class {
|
|
344
|
+
serverUrl;
|
|
345
|
+
apiKey;
|
|
346
|
+
constructor(config) {
|
|
347
|
+
this.serverUrl = config.serverUrl.replace(/\/$/, "");
|
|
348
|
+
this.apiKey = config.apiKey;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* 计算本地文件的 SHA-256 哈希值(流式计算)
|
|
352
|
+
* @deprecated 使用 calculateChunkedHash 替代
|
|
353
|
+
*/
|
|
354
|
+
async calculateFileHash(filePath) {
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
const hash = createHash("sha256");
|
|
357
|
+
const stream = createReadStream(filePath);
|
|
358
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
359
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
360
|
+
stream.on("error", reject);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* 计算分块哈希并返回最终哈希
|
|
365
|
+
* 与服务端保持一致:每个分块单独计算哈希,最后合并所有分块哈希再计算最终哈希
|
|
366
|
+
*/
|
|
367
|
+
async calculateChunkedHash(filePath, fileSize, chunkSize) {
|
|
368
|
+
const totalChunks = Math.ceil(fileSize / chunkSize);
|
|
369
|
+
const chunkHashes = [];
|
|
370
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
371
|
+
const start = i * chunkSize;
|
|
372
|
+
const length = Math.min(chunkSize, fileSize - start);
|
|
373
|
+
const chunkData = await this.readFileChunk(filePath, start, length);
|
|
374
|
+
const chunkHash = createHash("sha256").update(chunkData).digest("hex");
|
|
375
|
+
chunkHashes.push(chunkHash);
|
|
376
|
+
}
|
|
377
|
+
const combined = chunkHashes.join("");
|
|
378
|
+
return createHash("sha256").update(combined).digest("hex");
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* 读取文件指定范围的数据
|
|
382
|
+
*/
|
|
383
|
+
async readFileChunk(filePath, start, length) {
|
|
384
|
+
const fileHandle = await open(filePath, "r");
|
|
385
|
+
try {
|
|
386
|
+
const buffer = Buffer.alloc(length);
|
|
387
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, length, start);
|
|
388
|
+
return buffer.subarray(0, bytesRead);
|
|
389
|
+
} finally {
|
|
390
|
+
await fileHandle.close();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* 初始化上传会话
|
|
395
|
+
*/
|
|
396
|
+
async initUpload(request) {
|
|
397
|
+
const url = `${this.serverUrl}/api/sftp/chunked/init`;
|
|
398
|
+
const response = await fetch(url, {
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: {
|
|
401
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
402
|
+
"Content-Type": "application/json"
|
|
403
|
+
},
|
|
404
|
+
body: JSON.stringify(request)
|
|
405
|
+
});
|
|
406
|
+
return parseApiResponse(response);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* 上传单个分块
|
|
410
|
+
*/
|
|
411
|
+
async uploadChunk(sessionId, chunkIndex, chunkData) {
|
|
412
|
+
const url = `${this.serverUrl}/api/sftp/chunked/${sessionId}/chunk`;
|
|
413
|
+
const formData = new FormData();
|
|
414
|
+
formData.append("chunkIndex", String(chunkIndex));
|
|
415
|
+
const blob = new Blob([chunkData], { type: "application/octet-stream" });
|
|
416
|
+
formData.append("file", blob, `chunk-${chunkIndex}`);
|
|
417
|
+
const response = await fetch(url, {
|
|
418
|
+
method: "POST",
|
|
419
|
+
headers: {
|
|
420
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
421
|
+
},
|
|
422
|
+
body: formData
|
|
423
|
+
});
|
|
424
|
+
return parseApiResponse(response);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* 上传单个分块(带重试)
|
|
428
|
+
*/
|
|
429
|
+
async uploadChunkWithRetry(sessionId, chunkIndex, chunkData, totalChunks, retryConfig = DEFAULT_RETRY_CONFIG) {
|
|
430
|
+
let lastError = null;
|
|
431
|
+
for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
|
|
432
|
+
try {
|
|
433
|
+
return await this.uploadChunk(sessionId, chunkIndex, chunkData);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
436
|
+
if (!isRetryableError(err) || attempt >= retryConfig.maxRetries) {
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
const delay = calculateDelay(attempt, retryConfig);
|
|
440
|
+
console.error(LOG_PREFIX, `\u5206\u5757 ${chunkIndex + 1}/${totalChunks} \u91CD\u8BD5 (\u7B2C ${attempt + 1} \u6B21)`, {
|
|
441
|
+
delay: `${delay}ms`,
|
|
442
|
+
error: lastError.message
|
|
443
|
+
});
|
|
444
|
+
await sleep(delay);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
throw lastError;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* 完成上传并进行哈希校验
|
|
451
|
+
*/
|
|
452
|
+
async completeUpload(sessionId) {
|
|
453
|
+
const url = `${this.serverUrl}/api/sftp/chunked/${sessionId}/complete`;
|
|
454
|
+
const response = await fetch(url, {
|
|
455
|
+
method: "POST",
|
|
456
|
+
headers: {
|
|
457
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
return parseApiResponse(response);
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* 查询上传会话状态
|
|
464
|
+
*/
|
|
465
|
+
async getSessionStatus(sessionId) {
|
|
466
|
+
const url = `${this.serverUrl}/api/sftp/chunked/${sessionId}/status`;
|
|
467
|
+
const response = await fetch(url, {
|
|
468
|
+
method: "GET",
|
|
469
|
+
headers: {
|
|
470
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
return parseApiResponse(response);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 取消上传会话
|
|
477
|
+
*/
|
|
478
|
+
async cancelUpload(sessionId) {
|
|
479
|
+
const url = `${this.serverUrl}/api/sftp/chunked/${sessionId}/cancel`;
|
|
480
|
+
const response = await fetch(url, {
|
|
481
|
+
method: "POST",
|
|
482
|
+
headers: {
|
|
483
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
return parseApiResponse(response);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* 上传文件(完整流程)
|
|
490
|
+
*/
|
|
491
|
+
async upload(options) {
|
|
492
|
+
const {
|
|
493
|
+
credentialId,
|
|
494
|
+
remotePath,
|
|
495
|
+
localPath,
|
|
496
|
+
chunkSize = DEFAULT_CHUNK_SIZE,
|
|
497
|
+
overwrite = false
|
|
498
|
+
} = options;
|
|
499
|
+
let fileSize;
|
|
500
|
+
try {
|
|
501
|
+
const stat = statSync(localPath);
|
|
502
|
+
fileSize = stat.size;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
throw new ApiError(
|
|
505
|
+
ErrorCode.VALIDATION_ERROR,
|
|
506
|
+
`\u65E0\u6CD5\u8BFB\u53D6\u672C\u5730\u6587\u4EF6: ${localPath}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
if (fileSize > MAX_FILE_SIZE) {
|
|
510
|
+
throw new ApiError(
|
|
511
|
+
ErrorCode.VALIDATION_ERROR,
|
|
512
|
+
`\u6587\u4EF6\u5927\u5C0F\u8D85\u8FC7\u9650\u5236\uFF0C\u6700\u5927\u652F\u6301 ${MAX_FILE_SIZE / 1024 / 1024 / 1024}GB`
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const validChunkSize = Math.max(MIN_CHUNK_SIZE, Math.min(MAX_CHUNK_SIZE, chunkSize));
|
|
516
|
+
const lastSlash = remotePath.lastIndexOf("/");
|
|
517
|
+
const dirPath = lastSlash > 0 ? remotePath.substring(0, lastSlash) : "~";
|
|
518
|
+
const fileName = remotePath.substring(lastSlash + 1) || "file";
|
|
519
|
+
console.error(LOG_PREFIX, "\u8BA1\u7B97\u5206\u5757\u54C8\u5E0C...", { localPath, fileSize: formatSize(fileSize) });
|
|
520
|
+
const expectedHash = await this.calculateChunkedHash(localPath, fileSize, validChunkSize);
|
|
521
|
+
console.error(LOG_PREFIX, "\u521D\u59CB\u5316\u4E0A\u4F20\u4F1A\u8BDD...", { fileName, remotePath: dirPath });
|
|
522
|
+
const initResponse = await this.initUpload({
|
|
523
|
+
credentialId,
|
|
524
|
+
remotePath: dirPath,
|
|
525
|
+
fileName,
|
|
526
|
+
fileSize,
|
|
527
|
+
expectedHash,
|
|
528
|
+
chunkSize: validChunkSize,
|
|
529
|
+
overwrite
|
|
530
|
+
});
|
|
531
|
+
const { sessionId, totalChunks, uploadedChunks, resumed } = initResponse;
|
|
532
|
+
const skippedChunks = uploadedChunks.length;
|
|
533
|
+
console.error(LOG_PREFIX, "\u4F1A\u8BDD\u5DF2\u521B\u5EFA", {
|
|
534
|
+
sessionId: sessionId.slice(0, 8) + "...",
|
|
535
|
+
totalChunks,
|
|
536
|
+
resumed,
|
|
537
|
+
skippedChunks
|
|
538
|
+
});
|
|
539
|
+
if (resumed && skippedChunks > 0) {
|
|
540
|
+
console.error(LOG_PREFIX, `\u65AD\u70B9\u7EED\u4F20: \u8DF3\u8FC7 ${skippedChunks} \u4E2A\u5DF2\u4E0A\u4F20\u5206\u5757`);
|
|
541
|
+
}
|
|
542
|
+
const startTime = Date.now();
|
|
543
|
+
console.error(LOG_PREFIX, "\u5F00\u59CB\u4E0A\u4F20", {
|
|
544
|
+
fileSize: formatSize(fileSize),
|
|
545
|
+
totalChunks,
|
|
546
|
+
chunkSize: formatSize(validChunkSize)
|
|
547
|
+
});
|
|
548
|
+
let currentSessionId = sessionId;
|
|
549
|
+
let currentUploadedSet = new Set(uploadedChunks);
|
|
550
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
551
|
+
if (currentUploadedSet.has(i)) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const start = i * validChunkSize;
|
|
555
|
+
const length = Math.min(validChunkSize, fileSize - start);
|
|
556
|
+
const chunkData = await this.readFileChunk(localPath, start, length);
|
|
557
|
+
const uploadedCount = Array.from(currentUploadedSet).filter((idx) => idx < i).length + 1;
|
|
558
|
+
const pendingCount = totalChunks - skippedChunks;
|
|
559
|
+
const progress = Math.round(uploadedCount / pendingCount * 100);
|
|
560
|
+
console.error(LOG_PREFIX, `\u4E0A\u4F20\u5206\u5757 ${i + 1}/${totalChunks} (${progress}%)`, {
|
|
561
|
+
size: formatSize(length)
|
|
562
|
+
});
|
|
563
|
+
try {
|
|
564
|
+
await this.uploadChunkWithRetry(
|
|
565
|
+
currentSessionId,
|
|
566
|
+
i,
|
|
567
|
+
chunkData,
|
|
568
|
+
totalChunks
|
|
569
|
+
);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
if (isSessionExpiredError(err)) {
|
|
572
|
+
console.error(LOG_PREFIX, "\u4F1A\u8BDD\u5DF2\u8FC7\u671F\uFF0C\u5C1D\u8BD5\u6062\u590D...");
|
|
573
|
+
const newInitResponse = await this.initUpload({
|
|
574
|
+
credentialId,
|
|
575
|
+
remotePath: dirPath,
|
|
576
|
+
fileName,
|
|
577
|
+
fileSize,
|
|
578
|
+
expectedHash,
|
|
579
|
+
chunkSize: validChunkSize,
|
|
580
|
+
overwrite
|
|
581
|
+
});
|
|
582
|
+
currentSessionId = newInitResponse.sessionId;
|
|
583
|
+
currentUploadedSet = new Set(newInitResponse.uploadedChunks);
|
|
584
|
+
console.error(LOG_PREFIX, "\u4F1A\u8BDD\u5DF2\u6062\u590D", {
|
|
585
|
+
sessionId: currentSessionId.slice(0, 8) + "...",
|
|
586
|
+
uploadedChunks: newInitResponse.uploadedChunks.length
|
|
587
|
+
});
|
|
588
|
+
if (newInitResponse.resumed && newInitResponse.uploadedChunks.length > 0) {
|
|
589
|
+
console.error(LOG_PREFIX, `\u65AD\u70B9\u7EED\u4F20: \u8DF3\u8FC7 ${newInitResponse.uploadedChunks.length} \u4E2A\u5DF2\u4E0A\u4F20\u5206\u5757`);
|
|
590
|
+
}
|
|
591
|
+
if (currentUploadedSet.has(i)) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
await this.uploadChunkWithRetry(
|
|
595
|
+
currentSessionId,
|
|
596
|
+
i,
|
|
597
|
+
chunkData,
|
|
598
|
+
totalChunks
|
|
599
|
+
);
|
|
600
|
+
} else {
|
|
601
|
+
const errorMsg = formatErrorMessage(err);
|
|
602
|
+
console.error(LOG_PREFIX, `\u5206\u5757 ${i + 1}/${totalChunks} \u4E0A\u4F20\u5931\u8D25`, {
|
|
603
|
+
error: errorMsg,
|
|
604
|
+
uploadedChunks: i
|
|
605
|
+
});
|
|
606
|
+
throw new ApiError(
|
|
607
|
+
err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR,
|
|
608
|
+
`\u4E0A\u4F20\u5931\u8D25: ${errorMsg}\uFF0C\u5DF2\u4E0A\u4F20 ${i}/${totalChunks} \u5206\u5757`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
console.error(LOG_PREFIX, "\u5B8C\u6210\u4E0A\u4F20\uFF0C\u6821\u9A8C\u54C8\u5E0C...");
|
|
614
|
+
const completeResponse = await this.completeUpload(currentSessionId);
|
|
615
|
+
if (!completeResponse.verified) {
|
|
616
|
+
throw new ApiError(
|
|
617
|
+
ErrorCode.CHUNKED_HASH_MISMATCH,
|
|
618
|
+
"\u6587\u4EF6\u5B8C\u6574\u6027\u6821\u9A8C\u5931\u8D25"
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
const elapsed = Date.now() - startTime;
|
|
622
|
+
const elapsedSec = (elapsed / 1e3).toFixed(1);
|
|
623
|
+
console.error(LOG_PREFIX, "\u4E0A\u4F20\u5B8C\u6210", {
|
|
624
|
+
path: completeResponse.file.path,
|
|
625
|
+
size: formatSize(completeResponse.file.size),
|
|
626
|
+
elapsed: `${elapsedSec}s`
|
|
627
|
+
});
|
|
628
|
+
return {
|
|
629
|
+
success: true,
|
|
630
|
+
path: completeResponse.file.path,
|
|
631
|
+
size: completeResponse.file.size,
|
|
632
|
+
hash: completeResponse.file.hash,
|
|
633
|
+
resumed,
|
|
634
|
+
skippedChunks
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// src/index.ts
|
|
640
|
+
function formatFileSize(bytes) {
|
|
641
|
+
if (bytes === 0) return "0 B";
|
|
642
|
+
const k = 1024;
|
|
643
|
+
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
644
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
645
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
646
|
+
}
|
|
647
|
+
var SERVER_NAME = "auth-ssh-mcp";
|
|
648
|
+
var SERVER_VERSION = "1.0.0";
|
|
649
|
+
var LOCAL_TOOLS = /* @__PURE__ */ new Set(["sftp_chunked_upload"]);
|
|
650
|
+
function validateEnvironment() {
|
|
651
|
+
const serverUrl = process.env.MCP_SERVER_URL;
|
|
652
|
+
const apiKey = process.env.MCP_API_KEY;
|
|
653
|
+
if (!serverUrl) {
|
|
654
|
+
console.error("\u9519\u8BEF: \u7F3A\u5C11\u73AF\u5883\u53D8\u91CF MCP_SERVER_URL");
|
|
655
|
+
console.error("\u8BF7\u8BBE\u7F6E MCP_SERVER_URL \u4E3A auth-ssh \u540E\u7AEF\u670D\u52A1\u7684 URL");
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
if (!apiKey) {
|
|
659
|
+
console.error("\u9519\u8BEF: \u7F3A\u5C11\u73AF\u5883\u53D8\u91CF MCP_API_KEY");
|
|
660
|
+
console.error("\u8BF7\u8BBE\u7F6E MCP_API_KEY \u4E3A\u6709\u6548\u7684 API Key");
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
if (!apiKey.startsWith("sk_")) {
|
|
664
|
+
console.error("\u9519\u8BEF: API Key \u683C\u5F0F\u65E0\u6548");
|
|
665
|
+
console.error("API Key \u5E94\u4EE5 sk_ \u5F00\u5934");
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
return { serverUrl, apiKey };
|
|
669
|
+
}
|
|
670
|
+
async function createServer(mcpClient, chunkedUploadClient) {
|
|
671
|
+
const mcpServer = new McpServer({
|
|
672
|
+
name: SERVER_NAME,
|
|
673
|
+
version: SERVER_VERSION
|
|
674
|
+
});
|
|
675
|
+
const { tools: remoteTools } = await mcpClient.listTools();
|
|
676
|
+
const filteredRemoteTools = remoteTools.filter((tool) => {
|
|
677
|
+
if (LOCAL_TOOLS.has(tool.name)) {
|
|
678
|
+
console.error(`[${SERVER_NAME}] \u8DF3\u8FC7\u8FDC\u7A0B\u5DE5\u5177 ${tool.name}\uFF0C\u4F7F\u7528\u672C\u5730\u5B9E\u73B0`);
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
console.error(`[${SERVER_NAME}] \u6CE8\u518C\u8FDC\u7A0B\u5DE5\u5177: ${tool.name}`);
|
|
682
|
+
return true;
|
|
683
|
+
});
|
|
684
|
+
mcpServer.registerTool(
|
|
685
|
+
"_placeholder",
|
|
686
|
+
{ description: "placeholder", inputSchema: {} },
|
|
687
|
+
async () => ({ content: [{ type: "text", text: "" }] })
|
|
688
|
+
);
|
|
689
|
+
const localToolDef = {
|
|
690
|
+
name: "sftp_chunked_upload",
|
|
691
|
+
description: "\u5C06\u672C\u5730\u6587\u4EF6\u5206\u5757\u4E0A\u4F20\u5230\u8FDC\u7A0B\u670D\u52A1\u5668(\u652F\u6301\u65AD\u70B9\u7EED\u4F20\u548CSHA-256\u6821\u9A8C,\u6700\u5927500MB)",
|
|
692
|
+
inputSchema: {
|
|
693
|
+
type: "object",
|
|
694
|
+
properties: {
|
|
695
|
+
credentialId: {
|
|
696
|
+
type: "string",
|
|
697
|
+
format: "uuid",
|
|
698
|
+
description: "SSH \u51ED\u8BC1 ID"
|
|
699
|
+
},
|
|
700
|
+
localPath: {
|
|
701
|
+
type: "string",
|
|
702
|
+
minLength: 1,
|
|
703
|
+
description: "\u672C\u5730\u6587\u4EF6\u7EDD\u5BF9\u8DEF\u5F84,\u5982 /tmp/backup.tar.gz"
|
|
704
|
+
},
|
|
705
|
+
path: {
|
|
706
|
+
type: "string",
|
|
707
|
+
minLength: 1,
|
|
708
|
+
description: "\u8FDC\u7A0B\u76EE\u6807\u8DEF\u5F84,\u5982 /home/user/backup.tar.gz"
|
|
709
|
+
},
|
|
710
|
+
chunkSize: {
|
|
711
|
+
type: "integer",
|
|
712
|
+
default: 5242880,
|
|
713
|
+
description: "\u5206\u5757\u5927\u5C0F(\u5B57\u8282),\u9ED8\u8BA45MB,\u8303\u56F41MB-10MB"
|
|
714
|
+
},
|
|
715
|
+
overwrite: {
|
|
716
|
+
type: "boolean",
|
|
717
|
+
default: false,
|
|
718
|
+
description: "\u8FDC\u7A0B\u6587\u4EF6\u5DF2\u5B58\u5728\u65F6\u662F\u5426\u8986\u76D6"
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
required: ["credentialId", "localPath", "path"]
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
console.error(`[${SERVER_NAME}] \u6CE8\u518C\u672C\u5730\u5DE5\u5177: sftp_chunked_upload`);
|
|
725
|
+
const allTools = [...filteredRemoteTools, localToolDef];
|
|
726
|
+
mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
727
|
+
return { tools: allTools };
|
|
728
|
+
});
|
|
729
|
+
mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
730
|
+
const { name, arguments: args = {} } = request.params;
|
|
731
|
+
if (name === "sftp_chunked_upload") {
|
|
732
|
+
const { credentialId, path, localPath, chunkSize, overwrite } = args;
|
|
733
|
+
try {
|
|
734
|
+
const result = await chunkedUploadClient.upload({
|
|
735
|
+
credentialId,
|
|
736
|
+
remotePath: path,
|
|
737
|
+
localPath,
|
|
738
|
+
chunkSize,
|
|
739
|
+
overwrite
|
|
740
|
+
});
|
|
741
|
+
const sizeHuman = formatFileSize(result.size);
|
|
742
|
+
let text = `\u6587\u4EF6\u5DF2\u6210\u529F\u4E0A\u4F20 \`${result.path}\` (${sizeHuman})`;
|
|
743
|
+
if (result.resumed) {
|
|
744
|
+
text += `
|
|
745
|
+
\u65AD\u70B9\u7EED\u4F20: \u8DF3\u8FC7 ${result.skippedChunks} \u4E2A\u5DF2\u4E0A\u4F20\u5206\u5757`;
|
|
746
|
+
}
|
|
747
|
+
return {
|
|
748
|
+
content: [{ type: "text", text }]
|
|
749
|
+
};
|
|
750
|
+
} catch (error) {
|
|
751
|
+
return {
|
|
752
|
+
content: [{ type: "text", text: formatErrorMessage(error) }],
|
|
753
|
+
isError: true
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
const result = await mcpClient.callTool(name, args);
|
|
759
|
+
return result;
|
|
760
|
+
} catch (error) {
|
|
761
|
+
return {
|
|
762
|
+
content: [{ type: "text", text: formatErrorMessage(error) }],
|
|
763
|
+
isError: true
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
return mcpServer;
|
|
768
|
+
}
|
|
769
|
+
async function main() {
|
|
770
|
+
const { serverUrl, apiKey } = validateEnvironment();
|
|
771
|
+
console.error(`[${SERVER_NAME}] v${SERVER_VERSION}`);
|
|
772
|
+
console.error(`[${SERVER_NAME}] \u540E\u7AEF\u670D\u52A1: ${serverUrl}`);
|
|
773
|
+
const mcpClient = new McpClient({ serverUrl, apiKey });
|
|
774
|
+
try {
|
|
775
|
+
await mcpClient.connect();
|
|
776
|
+
} catch (error) {
|
|
777
|
+
console.error(`[${SERVER_NAME}] \u8FDE\u63A5\u5931\u8D25:`, error instanceof Error ? error.message : error);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
const chunkedUploadClient = new ChunkedUploadClient({ serverUrl, apiKey });
|
|
781
|
+
const server = await createServer(mcpClient, chunkedUploadClient);
|
|
782
|
+
const transport = new StdioServerTransport();
|
|
783
|
+
await server.connect(transport);
|
|
784
|
+
console.error(`[${SERVER_NAME}] \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7B49\u5F85\u8FDE\u63A5...`);
|
|
785
|
+
const shutdown = async () => {
|
|
786
|
+
console.error(`[${SERVER_NAME}] \u6B63\u5728\u5173\u95ED...`);
|
|
787
|
+
await mcpClient.close();
|
|
788
|
+
process.exit(0);
|
|
789
|
+
};
|
|
790
|
+
process.on("SIGINT", shutdown);
|
|
791
|
+
process.on("SIGTERM", shutdown);
|
|
792
|
+
}
|
|
793
|
+
main().catch((error) => {
|
|
794
|
+
console.error(`[${SERVER_NAME}] \u542F\u52A8\u5931\u8D25:`, error);
|
|
795
|
+
process.exit(1);
|
|
796
|
+
});
|
|
797
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/mcp-client.ts","../src/error-codes.ts","../src/error-handler.ts","../src/chunked-upload-client.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * auth-ssh-mcp - MCP Proxy for SSH credential management\n *\n * 环境变量:\n * - MCP_SERVER_URL: auth-ssh 后端服务 URL (必需)\n * - MCP_API_KEY: API Key (必需)\n *\n * 使用方式:\n * npx auth-ssh-mcp\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport {\n ListToolsRequestSchema,\n CallToolRequestSchema\n} from '@modelcontextprotocol/sdk/types.js'\nimport type { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js'\nimport { McpClient } from './mcp-client.js'\nimport { ChunkedUploadClient } from './chunked-upload-client.js'\nimport { formatErrorMessage } from './error-handler.js'\n\n/** 格式化文件大小 */\nfunction formatFileSize(bytes: number): string {\n if (bytes === 0) return '0 B'\n const k = 1024\n const sizes = ['B', 'KB', 'MB', 'GB', 'TB']\n const i = Math.floor(Math.log(bytes) / Math.log(k))\n return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]\n}\n\nconst SERVER_NAME = 'auth-ssh-mcp'\nconst SERVER_VERSION = '1.0.0'\n\n// 本地工具名称(不从远程获取)\nconst LOCAL_TOOLS = new Set(['sftp_chunked_upload'])\n\n/**\n * 验证环境变量\n */\nfunction validateEnvironment(): { serverUrl: string; apiKey: string } {\n const serverUrl = process.env.MCP_SERVER_URL\n const apiKey = process.env.MCP_API_KEY\n\n if (!serverUrl) {\n console.error('错误: 缺少环境变量 MCP_SERVER_URL')\n console.error('请设置 MCP_SERVER_URL 为 auth-ssh 后端服务的 URL')\n process.exit(1)\n }\n\n if (!apiKey) {\n console.error('错误: 缺少环境变量 MCP_API_KEY')\n console.error('请设置 MCP_API_KEY 为有效的 API Key')\n process.exit(1)\n }\n\n if (!apiKey.startsWith('sk_')) {\n console.error('错误: API Key 格式无效')\n console.error('API Key 应以 sk_ 开头')\n process.exit(1)\n }\n\n return { serverUrl, apiKey }\n}\n\n/**\n * 创建 MCP Server 并注册工具\n */\nasync function createServer(\n mcpClient: McpClient,\n chunkedUploadClient: ChunkedUploadClient\n): Promise<McpServer> {\n const mcpServer = new McpServer({\n name: SERVER_NAME,\n version: SERVER_VERSION\n })\n\n // 获取远程工具列表\n const { tools: remoteTools } = await mcpClient.listTools()\n\n // 过滤掉本地工具(如果远程也有同名工具)\n const filteredRemoteTools = remoteTools.filter((tool) => {\n if (LOCAL_TOOLS.has(tool.name)) {\n console.error(`[${SERVER_NAME}] 跳过远程工具 ${tool.name},使用本地实现`)\n return false\n }\n console.error(`[${SERVER_NAME}] 注册远程工具: ${tool.name}`)\n return true\n })\n\n // 注册占位工具来启用 tools capability(使用 registerTool 替代已弃用的 tool 方法)\n mcpServer.registerTool(\n '_placeholder',\n { description: 'placeholder', inputSchema: {} },\n async () => ({ content: [{ type: 'text' as const, text: '' }] })\n )\n\n // 本地工具定义(JSON Schema 格式)\n const localToolDef: Tool = {\n name: 'sftp_chunked_upload',\n description: '将本地文件分块上传到远程服务器(支持断点续传和SHA-256校验,最大500MB)',\n inputSchema: {\n type: 'object',\n properties: {\n credentialId: {\n type: 'string',\n format: 'uuid',\n description: 'SSH 凭证 ID'\n },\n localPath: {\n type: 'string',\n minLength: 1,\n description: '本地文件绝对路径,如 /tmp/backup.tar.gz'\n },\n path: {\n type: 'string',\n minLength: 1,\n description: '远程目标路径,如 /home/user/backup.tar.gz'\n },\n chunkSize: {\n type: 'integer',\n default: 5242880,\n description: '分块大小(字节),默认5MB,范围1MB-10MB'\n },\n overwrite: {\n type: 'boolean',\n default: false,\n description: '远程文件已存在时是否覆盖'\n }\n },\n required: ['credentialId', 'localPath', 'path']\n }\n }\n console.error(`[${SERVER_NAME}] 注册本地工具: sftp_chunked_upload`)\n\n // 合并远程工具和本地工具\n const allTools: Tool[] = [...filteredRemoteTools, localToolDef]\n\n // 覆盖 tools/list 请求处理器\n mcpServer.server.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools: allTools }\n })\n\n // 覆盖 tools/call 请求处理器\n mcpServer.server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {\n const { name, arguments: args = {} } = request.params\n\n // 本地工具 - sftp_chunked_upload\n if (name === 'sftp_chunked_upload') {\n const { credentialId, path, localPath, chunkSize, overwrite } = args as {\n credentialId: string\n path: string\n localPath: string\n chunkSize?: number\n overwrite?: boolean\n }\n\n try {\n const result = await chunkedUploadClient.upload({\n credentialId,\n remotePath: path,\n localPath,\n chunkSize,\n overwrite\n })\n\n const sizeHuman = formatFileSize(result.size)\n let text = `文件已成功上传 \\`${result.path}\\` (${sizeHuman})`\n if (result.resumed) {\n text += `\\n断点续传: 跳过 ${result.skippedChunks} 个已上传分块`\n }\n\n return {\n content: [{ type: 'text', text }]\n }\n } catch (error) {\n return {\n content: [{ type: 'text', text: formatErrorMessage(error) }],\n isError: true\n }\n }\n }\n\n // 远程工具 - 转发到后端 MCP 服务\n try {\n const result = await mcpClient.callTool(name, args as Record<string, unknown>)\n return result as CallToolResult\n } catch (error) {\n return {\n content: [{ type: 'text', text: formatErrorMessage(error) }],\n isError: true\n }\n }\n })\n\n return mcpServer\n}\n\n/**\n * 主函数\n */\nasync function main() {\n const { serverUrl, apiKey } = validateEnvironment()\n\n console.error(`[${SERVER_NAME}] v${SERVER_VERSION}`)\n console.error(`[${SERVER_NAME}] 后端服务: ${serverUrl}`)\n\n // 创建 MCP 客户端并连接远程服务\n const mcpClient = new McpClient({ serverUrl, apiKey })\n\n try {\n await mcpClient.connect()\n } catch (error) {\n console.error(`[${SERVER_NAME}] 连接失败:`, error instanceof Error ? error.message : error)\n process.exit(1)\n }\n\n // 创建上传客户端\n const chunkedUploadClient = new ChunkedUploadClient({ serverUrl, apiKey })\n\n // 创建 MCP Server 并注册工具\n const server = await createServer(mcpClient, chunkedUploadClient)\n\n // 启动 stdio 传输\n const transport = new StdioServerTransport()\n await server.connect(transport)\n\n console.error(`[${SERVER_NAME}] 服务已启动,等待连接...`)\n\n // 优雅关闭\n const shutdown = async () => {\n console.error(`[${SERVER_NAME}] 正在关闭...`)\n await mcpClient.close()\n process.exit(0)\n }\n\n process.on('SIGINT', shutdown)\n process.on('SIGTERM', shutdown)\n}\n\nmain().catch((error) => {\n console.error(`[${SERVER_NAME}] 启动失败:`, error)\n process.exit(1)\n})\n","/**\n * MCP HTTP Transport 客户端\n * 用于连接远程 MCP Server 并转发工具调用\n */\n\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'\nimport type { Tool } from '@modelcontextprotocol/sdk/types.js'\nimport { ApiError, formatErrorMessage } from './error-handler.js'\nimport { ErrorCode } from './error-codes.js'\n\n// 定义工具调用结果类型(兼容 SDK 返回的联合类型)\nexport interface ToolCallResult {\n content: Array<{\n type: 'text' | 'image' | 'audio' | 'resource' | 'resource_link'\n text?: string\n data?: string\n mimeType?: string\n [key: string]: unknown\n }>\n structuredContent?: Record<string, unknown>\n isError?: boolean\n _meta?: Record<string, unknown>\n}\n\nconst CLIENT_NAME = 'auth-ssh-mcp'\nconst CLIENT_VERSION = '1.0.0'\n\nexport interface McpClientConfig {\n serverUrl: string\n apiKey: string\n}\n\n/**\n * MCP HTTP Transport 客户端\n * 封装与远程 MCP Server 的通信\n */\nexport class McpClient {\n private client: Client\n private transport: StreamableHTTPClientTransport | null = null\n private serverUrl: string\n private apiKey: string\n private connected: boolean = false\n\n constructor(config: McpClientConfig) {\n this.serverUrl = config.serverUrl.replace(/\\/$/, '')\n this.apiKey = config.apiKey\n this.client = new Client({\n name: CLIENT_NAME,\n version: CLIENT_VERSION\n })\n }\n\n /**\n * 连接到远程 MCP Server\n */\n async connect(): Promise<void> {\n if (this.connected) {\n return\n }\n\n try {\n const mcpUrl = new URL(`${this.serverUrl}/api/mcp`)\n\n this.transport = new StreamableHTTPClientTransport(mcpUrl, {\n requestInit: {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`\n }\n }\n })\n\n await this.client.connect(this.transport)\n this.connected = true\n console.error(`[${CLIENT_NAME}] 已连接到远程 MCP Server`)\n } catch (error) {\n this.connected = false\n throw this.wrapError(error, '连接远程服务失败')\n }\n }\n\n /**\n * 获取远程工具列表\n */\n async listTools(): Promise<{ tools: Tool[] }> {\n if (!this.connected) {\n throw new Error('未连接到远程服务,请先调用 connect()')\n }\n\n try {\n const result = await this.client.listTools()\n console.error(`[${CLIENT_NAME}] 获取到 ${result.tools.length} 个远程工具`)\n return result\n } catch (error) {\n throw this.wrapError(error, '获取工具列表失败')\n }\n }\n\n /**\n * 调用远程工具\n */\n async callTool(name: string, args: Record<string, unknown>): Promise<ToolCallResult> {\n if (!this.connected) {\n throw new Error('未连接到远程服务,请先调用 connect()')\n }\n\n try {\n const result = await this.client.callTool({ name, arguments: args }, CallToolResultSchema)\n return result as ToolCallResult\n } catch (error) {\n throw this.wrapError(error, `调用工具 ${name} 失败`)\n }\n }\n\n /**\n * 关闭连接\n */\n async close(): Promise<void> {\n if (!this.connected) {\n return\n }\n\n try {\n await this.client.close()\n this.connected = false\n this.transport = null\n console.error(`[${CLIENT_NAME}] 已断开连接`)\n } catch (error) {\n // 忽略关闭时的错误\n console.error(`[${CLIENT_NAME}] 关闭连接时出错:`, error)\n this.connected = false\n this.transport = null\n }\n }\n\n /**\n * 检查连接状态\n */\n isConnected(): boolean {\n return this.connected\n }\n\n /**\n * 包装错误,转换为 ApiError\n */\n private wrapError(error: unknown, context: string): ApiError {\n // 如果已经是 ApiError,直接返回\n if (error instanceof ApiError) {\n return error\n }\n\n const originalMessage = error instanceof Error ? error.message : String(error)\n\n // 识别认证错误\n if (originalMessage.includes('401') || originalMessage.includes('403') || originalMessage.includes('Unauthorized')) {\n return new ApiError(ErrorCode.AUTH_UNAUTHORIZED, 'API Key 无效或已被撤销', 401)\n }\n\n // 识别网络错误\n if (originalMessage.includes('ECONNREFUSED') || originalMessage.includes('ENOTFOUND') || originalMessage.includes('ETIMEDOUT')) {\n return new ApiError(ErrorCode.CREDENTIAL_CONNECTION_FAILED, '无法连接到远程服务,请检查网络连接和服务地址')\n }\n\n // 识别会话错误\n if (originalMessage.includes('404') || originalMessage.includes('Session not found')) {\n return new ApiError(ErrorCode.CHUNKED_SESSION_NOT_FOUND, '会话已过期', 404)\n }\n\n // 识别服务端错误\n if (originalMessage.includes('500') || originalMessage.includes('502') || originalMessage.includes('503')) {\n return new ApiError(ErrorCode.INTERNAL_ERROR, '远程服务暂时不可用,请稍后重试', 500)\n }\n\n // 默认错误\n return new ApiError(ErrorCode.INTERNAL_ERROR, `${context}: ${originalMessage}`)\n }\n}\n","/**\n * 错误码枚举定义\n */\n\n/** 错误码常量 */\nexport const ErrorCode = {\n // 认证模块\n AUTH_UNAUTHORIZED: 'AUTH_UNAUTHORIZED',\n AUTH_FORBIDDEN: 'AUTH_FORBIDDEN',\n AUTH_TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',\n\n // 验证模块\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n\n // 凭证模块\n CREDENTIAL_NOT_FOUND: 'CREDENTIAL_NOT_FOUND',\n CREDENTIAL_CONNECTION_FAILED: 'CREDENTIAL_CONNECTION_FAILED',\n\n // 文件模块\n FILE_NOT_FOUND: 'FILE_NOT_FOUND',\n FILE_ALREADY_EXISTS: 'FILE_ALREADY_EXISTS',\n FILE_PERMISSION_DENIED: 'FILE_PERMISSION_DENIED',\n\n // 分块上传模块\n CHUNKED_SESSION_NOT_FOUND: 'CHUNKED_SESSION_NOT_FOUND',\n CHUNKED_SESSION_LIMIT_EXCEEDED: 'CHUNKED_SESSION_LIMIT_EXCEEDED',\n CHUNKED_INVALID_CHUNK_INDEX: 'CHUNKED_INVALID_CHUNK_INDEX',\n CHUNKED_INVALID_CHUNK_SIZE: 'CHUNKED_INVALID_CHUNK_SIZE',\n CHUNKED_INCOMPLETE_UPLOAD: 'CHUNKED_INCOMPLETE_UPLOAD',\n CHUNKED_HASH_MISMATCH: 'CHUNKED_HASH_MISMATCH',\n CHUNKED_IDLE_TIMEOUT: 'CHUNKED_IDLE_TIMEOUT',\n SFTP_WRITE_TIMEOUT: 'SFTP_WRITE_TIMEOUT',\n SFTP_CONNECTION_CLOSED: 'SFTP_CONNECTION_CLOSED',\n\n // 内部错误\n INTERNAL_ERROR: 'INTERNAL_ERROR'\n} as const\n\nexport type ErrorCodeType = typeof ErrorCode[keyof typeof ErrorCode]\n\n/** 错误码到中文消息映射 */\nexport const ErrorMessages: Record<string, string> = {\n // 认证模块\n [ErrorCode.AUTH_UNAUTHORIZED]: '需要 API Key 认证',\n [ErrorCode.AUTH_FORBIDDEN]: '无权访问此资源',\n [ErrorCode.AUTH_TOKEN_EXPIRED]: 'API Key 已过期',\n\n // 验证模块\n [ErrorCode.VALIDATION_ERROR]: '参数验证失败',\n\n // 凭证模块\n [ErrorCode.CREDENTIAL_NOT_FOUND]: '凭证不存在',\n [ErrorCode.CREDENTIAL_CONNECTION_FAILED]: '无法连接到远程服务器',\n\n // 文件模块\n [ErrorCode.FILE_NOT_FOUND]: '文件或目录不存在',\n [ErrorCode.FILE_ALREADY_EXISTS]: '文件已存在',\n [ErrorCode.FILE_PERMISSION_DENIED]: '没有文件操作权限',\n\n // 分块上传模块\n [ErrorCode.CHUNKED_SESSION_NOT_FOUND]: '上传会话不存在或已过期',\n [ErrorCode.CHUNKED_SESSION_LIMIT_EXCEEDED]: '已达到最大并发上传数限制',\n [ErrorCode.CHUNKED_INVALID_CHUNK_INDEX]: '无效的分块索引',\n [ErrorCode.CHUNKED_INVALID_CHUNK_SIZE]: '无效的分块大小',\n [ErrorCode.CHUNKED_INCOMPLETE_UPLOAD]: '上传未完成,存在缺失的分块',\n [ErrorCode.CHUNKED_HASH_MISMATCH]: '文件完整性校验失败',\n [ErrorCode.CHUNKED_IDLE_TIMEOUT]: '上传空闲超时,数据流中断超过 60 秒',\n [ErrorCode.SFTP_WRITE_TIMEOUT]: 'SFTP 写入超时,请重试',\n [ErrorCode.SFTP_CONNECTION_CLOSED]: 'SFTP 连接已断开,请重试',\n\n // 内部错误\n [ErrorCode.INTERNAL_ERROR]: '服务器内部错误'\n}\n","/**\n * 统一错误处理模块\n * 适配后端响应格式,提供中文错误提示\n */\n\nimport { ErrorMessages, ErrorCode } from './error-codes.js'\nimport type { ApiErrorResponse, RetryConfig } from './types.js'\n\n/**\n * 错误分类枚举\n */\nexport const ErrorCategory = {\n /** 超时类错误 - 可重试 */\n RETRYABLE_TIMEOUT: 'RETRYABLE_TIMEOUT',\n /** 网络类错误 - 可重试 */\n RETRYABLE_NETWORK: 'RETRYABLE_NETWORK',\n /** 服务端错误 - 可重试 */\n RETRYABLE_SERVER: 'RETRYABLE_SERVER',\n /** 会话过期 - 需重新初始化 */\n SESSION_EXPIRED: 'SESSION_EXPIRED',\n /** 不可重试错误 */\n NON_RETRYABLE: 'NON_RETRYABLE'\n} as const\n\nexport type ErrorCategoryType = typeof ErrorCategory[keyof typeof ErrorCategory]\n\n/**\n * 错误码到分类的映射\n */\nconst ERROR_CATEGORY_MAP: Record<string, ErrorCategoryType> = {\n // 超时类 - 可重试\n [ErrorCode.CHUNKED_IDLE_TIMEOUT]: ErrorCategory.RETRYABLE_TIMEOUT,\n [ErrorCode.SFTP_WRITE_TIMEOUT]: ErrorCategory.RETRYABLE_TIMEOUT,\n\n // 网络类 - 可重试\n [ErrorCode.SFTP_CONNECTION_CLOSED]: ErrorCategory.RETRYABLE_NETWORK,\n\n // 会话过期 - 需重新初始化\n [ErrorCode.CHUNKED_SESSION_NOT_FOUND]: ErrorCategory.SESSION_EXPIRED,\n\n // 不可重试\n [ErrorCode.FILE_ALREADY_EXISTS]: ErrorCategory.NON_RETRYABLE,\n [ErrorCode.CHUNKED_SESSION_LIMIT_EXCEEDED]: ErrorCategory.NON_RETRYABLE,\n [ErrorCode.CHUNKED_HASH_MISMATCH]: ErrorCategory.NON_RETRYABLE,\n [ErrorCode.CREDENTIAL_NOT_FOUND]: ErrorCategory.NON_RETRYABLE,\n [ErrorCode.VALIDATION_ERROR]: ErrorCategory.NON_RETRYABLE\n}\n\n/**\n * API 错误类\n */\nexport class ApiError extends Error {\n readonly code: string\n readonly statusCode?: number\n readonly details?: unknown\n\n constructor(code: string, message?: string, statusCode?: number, details?: unknown) {\n // 优先使用传入的 message,否则使用错误码映射\n const finalMessage = message || ErrorMessages[code] || code\n super(finalMessage)\n this.name = 'ApiError'\n this.code = code\n this.statusCode = statusCode\n this.details = details\n }\n}\n\n/**\n * 解析 API 响应\n * 如果响应包含错误,抛出 ApiError\n */\nexport async function parseApiResponse<T>(response: Response): Promise<T> {\n const contentType = response.headers.get('content-type') || ''\n\n // 非 JSON 响应\n if (!contentType.includes('application/json')) {\n if (!response.ok) {\n throw new ApiError(\n 'INTERNAL_ERROR',\n `HTTP ${response.status}: ${response.statusText}`,\n response.status\n )\n }\n // 对于非 JSON 成功响应,返回空对象\n return {} as T\n }\n\n const json: unknown = await response.json()\n\n // 检查是否为错误响应\n if (!response.ok || (typeof json === 'object' && json !== null && 'error' in json)) {\n const errorResponse = json as ApiErrorResponse\n const error = errorResponse.error || { code: 'INTERNAL_ERROR', message: '' }\n\n throw new ApiError(\n error.code,\n error.message || ErrorMessages[error.code],\n response.status,\n error.details\n )\n }\n\n // 成功响应,返回 data 字段\n if (typeof json === 'object' && json !== null && 'data' in json) {\n return (json as { data: T }).data\n }\n\n // 兼容直接返回数据的情况\n return json as T\n}\n\n/**\n * 格式化错误为用户友好的消息(包含调试信息)\n */\nexport function formatErrorMessage(error: unknown): string {\n if (error instanceof ApiError) {\n const parts: string[] = [error.message]\n\n // 添加错误码\n if (error.code && error.code !== error.message) {\n parts.push(`[${error.code}]`)\n }\n\n // 添加 HTTP 状态码\n if (error.statusCode) {\n parts.push(`(HTTP ${error.statusCode})`)\n }\n\n // 添加详细信息\n if (error.details) {\n parts.push(`\\n详情: ${JSON.stringify(error.details)}`)\n }\n\n return parts.join(' ')\n }\n\n if (error instanceof Error) {\n // 识别网络错误\n if (error.message.includes('ECONNREFUSED')) {\n return '无法连接到远程服务,请检查网络连接'\n }\n if (error.message.includes('ENOTFOUND')) {\n return '无法解析服务器地址'\n }\n if (error.message.includes('ETIMEDOUT')) {\n return '连接超时,请稍后重试'\n }\n if (error.message.includes('ECONNRESET')) {\n return '连接被重置,请稍后重试'\n }\n return error.message\n }\n\n return String(error)\n}\n\n/**\n * 判断是否为可重试的错误\n */\nexport function isRetryableError(error: unknown): boolean {\n if (error instanceof ApiError) {\n // 检查错误码映射\n const category = ERROR_CATEGORY_MAP[error.code]\n if (category === ErrorCategory.RETRYABLE_TIMEOUT ||\n category === ErrorCategory.RETRYABLE_NETWORK ||\n category === ErrorCategory.RETRYABLE_SERVER) {\n return true\n }\n\n // HTTP 408 (空闲超时) 可重试\n if (error.statusCode === 408) {\n return true\n }\n\n // HTTP 5xx (服务端错误) 可重试\n if (error.statusCode && error.statusCode >= 500) {\n return true\n }\n }\n\n if (error instanceof Error) {\n // 网络错误可重试\n if (error.message.includes('ECONNREFUSED') ||\n error.message.includes('ETIMEDOUT') ||\n error.message.includes('ECONNRESET')) {\n return true\n }\n }\n\n return false\n}\n\n/**\n * 判断是否为会话过期错误\n */\nexport function isSessionExpiredError(error: unknown): boolean {\n if (error instanceof ApiError) {\n return ERROR_CATEGORY_MAP[error.code] === ErrorCategory.SESSION_EXPIRED\n }\n return false\n}\n\n/**\n * 计算指数退避延迟\n * @param attempt 当前尝试次数(从 0 开始)\n * @param config 重试配置\n * @returns 延迟时间(毫秒)\n */\nexport function calculateDelay(attempt: number, config: RetryConfig): number {\n return config.baseDelay * Math.pow(config.multiplier, attempt)\n}\n\n/**\n * 格式化上传错误为用户友好的消息\n * @param error 错误对象\n * @param context 上下文信息(文件名、已上传进度等)\n */\nexport function formatUploadError(\n error: unknown,\n context?: { fileName?: string; uploadedChunks?: number; totalChunks?: number }\n): string {\n const baseMsg = formatErrorMessage(error)\n const parts: string[] = [baseMsg]\n\n if (context) {\n if (context.fileName) {\n parts.push(`文件: ${context.fileName}`)\n }\n if (context.uploadedChunks !== undefined && context.totalChunks !== undefined) {\n parts.push(`已上传: ${context.uploadedChunks}/${context.totalChunks} 分块`)\n }\n }\n\n // 添加建议操作\n if (error instanceof ApiError) {\n const category = ERROR_CATEGORY_MAP[error.code]\n if (category === ErrorCategory.SESSION_EXPIRED) {\n parts.push('建议: 重新上传,系统将自动恢复')\n } else if (category === ErrorCategory.NON_RETRYABLE) {\n if (error.code === ErrorCode.FILE_ALREADY_EXISTS) {\n parts.push('建议: 使用 overwrite=true 覆盖文件')\n } else if (error.code === ErrorCode.CHUNKED_SESSION_LIMIT_EXCEEDED) {\n parts.push('建议: 等待其他上传完成后重试')\n }\n }\n }\n\n return parts.join(' | ')\n}\n","/**\n * 分块上传客户端\n * 支持大文件分块上传、断点续传和 SHA-256 哈希校验\n */\n\nimport { createHash } from 'crypto'\nimport { createReadStream, statSync } from 'fs'\nimport { open } from 'fs/promises'\nimport {\n ApiError,\n parseApiResponse,\n isRetryableError,\n isSessionExpiredError,\n calculateDelay,\n formatErrorMessage\n} from './error-handler.js'\nimport { ErrorCode } from './error-codes.js'\nimport type {\n InitUploadRequest,\n InitUploadResponse,\n UploadChunkResponse,\n CompleteUploadResponse,\n SessionStatusResponse,\n CancelUploadResponse,\n RetryConfig\n} from './types.js'\n\n/** 日志前缀 */\nconst LOG_PREFIX = '[ChunkedUpload]'\n\n/** 格式化文件大小 */\nfunction formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes}B`\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`\n return `${(bytes / 1024 / 1024).toFixed(1)}MB`\n}\n\n/** 默认分块大小: 5MB */\nconst DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024\n\n/** 最小分块大小: 1MB */\nconst MIN_CHUNK_SIZE = 1 * 1024 * 1024\n\n/** 最大分块大小: 10MB */\nconst MAX_CHUNK_SIZE = 10 * 1024 * 1024\n\n/** 最大文件大小: 10GB */\nconst MAX_FILE_SIZE = 10 * 1024 * 1024 * 1024\n\n/** 默认重试配置 */\nconst DEFAULT_RETRY_CONFIG: RetryConfig = {\n maxRetries: 3,\n baseDelay: 1000,\n multiplier: 2\n}\n\n/** 延迟函数 */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms))\n}\n\nexport interface ChunkedUploadClientConfig {\n serverUrl: string\n apiKey: string\n}\n\nexport interface ChunkedUploadOptions {\n credentialId: string\n remotePath: string\n localPath: string\n chunkSize?: number\n overwrite?: boolean\n}\n\nexport interface ChunkedUploadResult {\n success: boolean\n path: string\n size: number\n hash: string\n resumed: boolean\n skippedChunks: number\n}\n\n/**\n * 分块上传客户端\n */\nexport class ChunkedUploadClient {\n private serverUrl: string\n private apiKey: string\n\n constructor(config: ChunkedUploadClientConfig) {\n this.serverUrl = config.serverUrl.replace(/\\/$/, '')\n this.apiKey = config.apiKey\n }\n\n /**\n * 计算本地文件的 SHA-256 哈希值(流式计算)\n * @deprecated 使用 calculateChunkedHash 替代\n */\n async calculateFileHash(filePath: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const hash = createHash('sha256')\n const stream = createReadStream(filePath)\n stream.on('data', (chunk) => hash.update(chunk))\n stream.on('end', () => resolve(hash.digest('hex')))\n stream.on('error', reject)\n })\n }\n\n /**\n * 计算分块哈希并返回最终哈希\n * 与服务端保持一致:每个分块单独计算哈希,最后合并所有分块哈希再计算最终哈希\n */\n async calculateChunkedHash(\n filePath: string,\n fileSize: number,\n chunkSize: number\n ): Promise<string> {\n const totalChunks = Math.ceil(fileSize / chunkSize)\n const chunkHashes: string[] = []\n\n for (let i = 0; i < totalChunks; i++) {\n const start = i * chunkSize\n const length = Math.min(chunkSize, fileSize - start)\n const chunkData = await this.readFileChunk(filePath, start, length)\n const chunkHash = createHash('sha256').update(chunkData).digest('hex')\n chunkHashes.push(chunkHash)\n }\n\n // 合并所有分块哈希,再计算最终哈希\n const combined = chunkHashes.join('')\n return createHash('sha256').update(combined).digest('hex')\n }\n\n /**\n * 读取文件指定范围的数据\n */\n async readFileChunk(filePath: string, start: number, length: number): Promise<Buffer> {\n const fileHandle = await open(filePath, 'r')\n try {\n const buffer = Buffer.alloc(length)\n const { bytesRead } = await fileHandle.read(buffer, 0, length, start)\n return buffer.subarray(0, bytesRead)\n } finally {\n await fileHandle.close()\n }\n }\n\n /**\n * 初始化上传会话\n */\n async initUpload(request: InitUploadRequest): Promise<InitUploadResponse> {\n const url = `${this.serverUrl}/api/sftp/chunked/init`\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(request)\n })\n\n return parseApiResponse<InitUploadResponse>(response)\n }\n\n /**\n * 上传单个分块\n */\n async uploadChunk(\n sessionId: string,\n chunkIndex: number,\n chunkData: Buffer\n ): Promise<UploadChunkResponse> {\n const url = `${this.serverUrl}/api/sftp/chunked/${sessionId}/chunk`\n\n const formData = new FormData()\n formData.append('chunkIndex', String(chunkIndex))\n const blob = new Blob([chunkData], { type: 'application/octet-stream' })\n formData.append('file', blob, `chunk-${chunkIndex}`)\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`\n },\n body: formData\n })\n\n return parseApiResponse<UploadChunkResponse>(response)\n }\n\n /**\n * 上传单个分块(带重试)\n */\n private async uploadChunkWithRetry(\n sessionId: string,\n chunkIndex: number,\n chunkData: Buffer,\n totalChunks: number,\n retryConfig: RetryConfig = DEFAULT_RETRY_CONFIG\n ): Promise<UploadChunkResponse> {\n let lastError: Error | null = null\n\n for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {\n try {\n return await this.uploadChunk(sessionId, chunkIndex, chunkData)\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err))\n\n // 检查是否可重试\n if (!isRetryableError(err) || attempt >= retryConfig.maxRetries) {\n break\n }\n\n // 计算延迟并等待\n const delay = calculateDelay(attempt, retryConfig)\n console.error(LOG_PREFIX, `分块 ${chunkIndex + 1}/${totalChunks} 重试 (第 ${attempt + 1} 次)`, {\n delay: `${delay}ms`,\n error: lastError.message\n })\n await sleep(delay)\n }\n }\n\n // 重试耗尽,抛出最后的错误\n throw lastError\n }\n\n /**\n * 完成上传并进行哈希校验\n */\n async completeUpload(sessionId: string): Promise<CompleteUploadResponse> {\n const url = `${this.serverUrl}/api/sftp/chunked/${sessionId}/complete`\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`\n }\n })\n\n return parseApiResponse<CompleteUploadResponse>(response)\n }\n\n /**\n * 查询上传会话状态\n */\n async getSessionStatus(sessionId: string): Promise<SessionStatusResponse> {\n const url = `${this.serverUrl}/api/sftp/chunked/${sessionId}/status`\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`\n }\n })\n\n return parseApiResponse<SessionStatusResponse>(response)\n }\n\n /**\n * 取消上传会话\n */\n async cancelUpload(sessionId: string): Promise<CancelUploadResponse> {\n const url = `${this.serverUrl}/api/sftp/chunked/${sessionId}/cancel`\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`\n }\n })\n\n return parseApiResponse<CancelUploadResponse>(response)\n }\n\n /**\n * 上传文件(完整流程)\n */\n async upload(options: ChunkedUploadOptions): Promise<ChunkedUploadResult> {\n const {\n credentialId,\n remotePath,\n localPath,\n chunkSize = DEFAULT_CHUNK_SIZE,\n overwrite = false\n } = options\n\n // 获取本地文件信息\n let fileSize: number\n try {\n const stat = statSync(localPath)\n fileSize = stat.size\n } catch (err) {\n throw new ApiError(\n ErrorCode.VALIDATION_ERROR,\n `无法读取本地文件: ${localPath}`\n )\n }\n\n // 检查文件大小\n if (fileSize > MAX_FILE_SIZE) {\n throw new ApiError(\n ErrorCode.VALIDATION_ERROR,\n `文件大小超过限制,最大支持 ${MAX_FILE_SIZE / 1024 / 1024 / 1024}GB`\n )\n }\n\n // 校正分块大小到有效范围\n const validChunkSize = Math.max(MIN_CHUNK_SIZE, Math.min(MAX_CHUNK_SIZE, chunkSize))\n\n // 从完整路径分离目录和文件名\n const lastSlash = remotePath.lastIndexOf('/')\n const dirPath = lastSlash > 0 ? remotePath.substring(0, lastSlash) : '~'\n const fileName = remotePath.substring(lastSlash + 1) || 'file'\n\n // 1. 计算分块哈希\n console.error(LOG_PREFIX, '计算分块哈希...', { localPath, fileSize: formatSize(fileSize) })\n const expectedHash = await this.calculateChunkedHash(localPath, fileSize, validChunkSize)\n\n // 2. 初始化上传会话\n console.error(LOG_PREFIX, '初始化上传会话...', { fileName, remotePath: dirPath })\n const initResponse = await this.initUpload({\n credentialId,\n remotePath: dirPath,\n fileName,\n fileSize,\n expectedHash,\n chunkSize: validChunkSize,\n overwrite\n })\n\n const { sessionId, totalChunks, uploadedChunks, resumed } = initResponse\n const skippedChunks = uploadedChunks.length\n\n console.error(LOG_PREFIX, '会话已创建', {\n sessionId: sessionId.slice(0, 8) + '...',\n totalChunks,\n resumed,\n skippedChunks\n })\n\n // 断点续传提示\n if (resumed && skippedChunks > 0) {\n console.error(LOG_PREFIX, `断点续传: 跳过 ${skippedChunks} 个已上传分块`)\n }\n\n // 记录上传开始时间\n const startTime = Date.now()\n console.error(LOG_PREFIX, '开始上传', {\n fileSize: formatSize(fileSize),\n totalChunks,\n chunkSize: formatSize(validChunkSize)\n })\n\n // 当前会话 ID(可能因会话恢复而变化)\n let currentSessionId = sessionId\n let currentUploadedSet = new Set(uploadedChunks)\n\n // 3. 上传分块(跳过已上传的,支持重试和会话恢复)\n for (let i = 0; i < totalChunks; i++) {\n if (currentUploadedSet.has(i)) {\n continue // 跳过已上传分块\n }\n\n const start = i * validChunkSize\n const length = Math.min(validChunkSize, fileSize - start)\n const chunkData = await this.readFileChunk(localPath, start, length)\n\n // 计算进度百分比\n const uploadedCount = Array.from(currentUploadedSet).filter(idx => idx < i).length + 1\n const pendingCount = totalChunks - skippedChunks\n const progress = Math.round((uploadedCount / pendingCount) * 100)\n\n console.error(LOG_PREFIX, `上传分块 ${i + 1}/${totalChunks} (${progress}%)`, {\n size: formatSize(length)\n })\n\n try {\n await this.uploadChunkWithRetry(\n currentSessionId,\n i,\n chunkData,\n totalChunks\n )\n } catch (err) {\n // 检查是否为会话过期错误\n if (isSessionExpiredError(err)) {\n console.error(LOG_PREFIX, '会话已过期,尝试恢复...')\n\n // 重新初始化会话\n const newInitResponse = await this.initUpload({\n credentialId,\n remotePath: dirPath,\n fileName,\n fileSize,\n expectedHash,\n chunkSize: validChunkSize,\n overwrite\n })\n\n currentSessionId = newInitResponse.sessionId\n currentUploadedSet = new Set(newInitResponse.uploadedChunks)\n\n console.error(LOG_PREFIX, '会话已恢复', {\n sessionId: currentSessionId.slice(0, 8) + '...',\n uploadedChunks: newInitResponse.uploadedChunks.length\n })\n\n if (newInitResponse.resumed && newInitResponse.uploadedChunks.length > 0) {\n console.error(LOG_PREFIX, `断点续传: 跳过 ${newInitResponse.uploadedChunks.length} 个已上传分块`)\n }\n\n // 如果当前分块已上传,跳过\n if (currentUploadedSet.has(i)) {\n continue\n }\n\n // 重试当前分块\n await this.uploadChunkWithRetry(\n currentSessionId,\n i,\n chunkData,\n totalChunks\n )\n } else {\n // 非会话过期错误,格式化错误信息并抛出\n const errorMsg = formatErrorMessage(err)\n console.error(LOG_PREFIX, `分块 ${i + 1}/${totalChunks} 上传失败`, {\n error: errorMsg,\n uploadedChunks: i\n })\n throw new ApiError(\n err instanceof ApiError ? err.code : ErrorCode.INTERNAL_ERROR,\n `上传失败: ${errorMsg},已上传 ${i}/${totalChunks} 分块`\n )\n }\n }\n }\n\n // 4. 完成上传\n console.error(LOG_PREFIX, '完成上传,校验哈希...')\n const completeResponse = await this.completeUpload(currentSessionId)\n\n if (!completeResponse.verified) {\n throw new ApiError(\n ErrorCode.CHUNKED_HASH_MISMATCH,\n '文件完整性校验失败'\n )\n }\n\n // 计算上传耗时\n const elapsed = Date.now() - startTime\n const elapsedSec = (elapsed / 1000).toFixed(1)\n console.error(LOG_PREFIX, '上传完成', {\n path: completeResponse.file.path,\n size: formatSize(completeResponse.file.size),\n elapsed: `${elapsedSec}s`\n })\n\n return {\n success: true,\n path: completeResponse.file.path,\n size: completeResponse.file.size,\n hash: completeResponse.file.hash,\n resumed,\n skippedChunks\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth-ssh-mcp",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "MCP Server for SSH credential management - connects to auth-ssh backend",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"auth-ssh-mcp": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"bin"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"dev": "tsup --watch",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "eslint src",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"ssh",
|
|
26
|
+
"sftp",
|
|
27
|
+
"claude",
|
|
28
|
+
"anthropic",
|
|
29
|
+
"model-context-protocol"
|
|
30
|
+
],
|
|
31
|
+
"author": "",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/your-username/auth-ssh-mcp.git"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
42
|
+
"zod": "^4.2.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^22.15.29",
|
|
46
|
+
"tsup": "^8.5.0",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
}
|
|
49
|
+
}
|