mcp-file-adapter 0.1.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.
Files changed (3) hide show
  1. package/README.md +37 -0
  2. package/dist/index.js +268 -0
  3. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # mcp-file-adapter
2
+
3
+ 本地 MCP stdio 适配器:将 4 个工具调用转为远程 HTTP 请求,并直接上传/下载文件。
4
+
5
+ ## 环境变量
6
+
7
+ - REMOTE_BASE_URL: 远程服务地址,默认 http://localhost:8080
8
+ - TIMEOUT_MS: 请求超时,默认 30000
9
+
10
+ ## 启动参数
11
+
12
+ - --remote-base-url=... 最高优先级
13
+
14
+ ## 鉴权
15
+
16
+ - 固定 token: mcp-file-service-token
17
+
18
+ ## 工具
19
+
20
+ - list_files { path }
21
+ - dir_mkdir { path, recursive? }
22
+ - upload_file { local_path, remote_path, overwrite?, mkdirs? }
23
+ - download_file { remote_path, local_path, overwrite?, mkdirs? }
24
+
25
+ ## 运行
26
+
27
+ ```bash
28
+ npm install
29
+ npm run dev
30
+ ```
31
+
32
+ ## 发布
33
+
34
+ ```bash
35
+ npm run build
36
+ npm publish
37
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ import { createReadStream, createWriteStream, promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { Readable } from "node:stream";
5
+ import { pipeline } from "node:stream/promises";
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { z } from "zod";
9
+ function readArg(name) {
10
+ const prefix = `--${name}=`;
11
+ for (const arg of process.argv.slice(2)) {
12
+ if (arg === `--${name}`)
13
+ return "true";
14
+ if (arg.startsWith(prefix))
15
+ return arg.slice(prefix.length);
16
+ }
17
+ return undefined;
18
+ }
19
+ const REMOTE_BASE_URL = readArg("remote-base-url") || process.env.REMOTE_BASE_URL || "http://localhost:8080";
20
+ const AUTH_TOKEN = "mcp-file-service-token";
21
+ const TIMEOUT_MS = Number(process.env.TIMEOUT_MS || 30000);
22
+ function ok(result, message) {
23
+ return {
24
+ content: [{ type: "text", text: message ?? JSON.stringify(result) }],
25
+ structuredContent: result,
26
+ };
27
+ }
28
+ function fail(error, code) {
29
+ const result = { ok: false, error, code };
30
+ return {
31
+ content: [{ type: "text", text: `error: ${error}` }],
32
+ structuredContent: result,
33
+ };
34
+ }
35
+ function resolveLocalPath(localPath) {
36
+ return path.resolve(process.cwd(), localPath);
37
+ }
38
+ function buildUrl(pathname, params) {
39
+ const url = new URL(pathname, REMOTE_BASE_URL);
40
+ Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
41
+ return url.toString();
42
+ }
43
+ async function fetchWithTimeout(url, init) {
44
+ const controller = new AbortController();
45
+ const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
46
+ try {
47
+ return await fetch(url, { ...init, signal: controller.signal });
48
+ }
49
+ finally {
50
+ clearTimeout(timeout);
51
+ }
52
+ }
53
+ async function parseJsonResponse(res) {
54
+ const text = await res.text();
55
+ if (!text)
56
+ return {};
57
+ return JSON.parse(text);
58
+ }
59
+ async function failFromResponse(res) {
60
+ try {
61
+ const data = await parseJsonResponse(res);
62
+ if (data && data.ok === false && typeof data.error === "string") {
63
+ return fail(String(data.error), typeof data.code === "string" ? data.code : undefined);
64
+ }
65
+ }
66
+ catch {
67
+ // ignore
68
+ }
69
+ return fail(`remote error: ${res.status} ${res.statusText}`);
70
+ }
71
+ async function postJson(pathname, body) {
72
+ const url = new URL(pathname, REMOTE_BASE_URL);
73
+ const res = await fetchWithTimeout(url.toString(), {
74
+ method: "POST",
75
+ headers: {
76
+ Authorization: `Bearer ${AUTH_TOKEN}`,
77
+ "Content-Type": "application/json",
78
+ },
79
+ body: JSON.stringify(body),
80
+ });
81
+ if (!res.ok)
82
+ return failFromResponse(res);
83
+ const data = await parseJsonResponse(res);
84
+ if (data.ok === false && typeof data.error === "string") {
85
+ return fail(String(data.error), typeof data.code === "string" ? data.code : undefined);
86
+ }
87
+ return ok(data);
88
+ }
89
+ function createServer() {
90
+ const server = new McpServer({
91
+ name: "mcp-file-adapter",
92
+ version: "0.1.0",
93
+ });
94
+ const errorSchema = z.object({
95
+ ok: z.literal(false),
96
+ error: z.string(),
97
+ code: z.string().optional(),
98
+ });
99
+ server.registerTool("list_files", {
100
+ description: "列出远程目录下的条目;返回 name/type/size/mtimeMs。",
101
+ inputSchema: z.object({
102
+ path: z.string(),
103
+ }),
104
+ outputSchema: z.union([
105
+ z.object({
106
+ ok: z.literal(true),
107
+ entries: z.array(z.object({
108
+ name: z.string(),
109
+ type: z.enum(["file", "dir"]),
110
+ size: z.number().optional(),
111
+ mtimeMs: z.number(),
112
+ })),
113
+ }),
114
+ errorSchema,
115
+ ]),
116
+ }, async ({ path: relPath }) => {
117
+ const url = buildUrl("/list", { path: relPath });
118
+ const res = await fetchWithTimeout(url, {
119
+ method: "GET",
120
+ headers: { Authorization: `Bearer ${AUTH_TOKEN}` },
121
+ });
122
+ if (!res.ok)
123
+ return failFromResponse(res);
124
+ const data = await parseJsonResponse(res);
125
+ if (data.ok === false && typeof data.error === "string") {
126
+ return fail(String(data.error), typeof data.code === "string" ? data.code : undefined);
127
+ }
128
+ return ok(data);
129
+ });
130
+ server.registerTool("dir_mkdir", {
131
+ description: "在远程创建目录;path 为相对路径,recursive 默认为 true。",
132
+ inputSchema: z.object({
133
+ path: z.string(),
134
+ recursive: z.boolean().optional(),
135
+ }),
136
+ outputSchema: z.union([
137
+ z.object({
138
+ ok: z.literal(true),
139
+ }),
140
+ errorSchema,
141
+ ]),
142
+ }, async ({ path: relPath, recursive }) => {
143
+ return postJson("/mkdir", { path: relPath, recursive: recursive ?? true });
144
+ });
145
+ server.registerTool("upload_file", {
146
+ description: "上传本地文件到远程;local_path 为本地路径,remote_path 为远程相对路径。",
147
+ inputSchema: z.object({
148
+ local_path: z.string(),
149
+ remote_path: z.string(),
150
+ overwrite: z.boolean().optional(),
151
+ mkdirs: z.boolean().optional(),
152
+ }),
153
+ outputSchema: z.union([
154
+ z.object({
155
+ ok: z.literal(true),
156
+ remote_path: z.string(),
157
+ local_path: z.string(),
158
+ size: z.number(),
159
+ }),
160
+ errorSchema,
161
+ ]),
162
+ }, async ({ local_path: localPath, remote_path: remotePath, overwrite, mkdirs, }) => {
163
+ try {
164
+ const localFull = resolveLocalPath(localPath);
165
+ const stat = await fs.stat(localFull);
166
+ if (!stat.isFile())
167
+ return fail("invalid local path", "EISDIR");
168
+ const url = buildUrl("/upload", {
169
+ path: remotePath,
170
+ overwrite: overwrite ? "1" : "0",
171
+ mkdirs: mkdirs ? "1" : "0",
172
+ });
173
+ const body = Readable.toWeb(createReadStream(localFull));
174
+ const res = await fetchWithTimeout(url, {
175
+ method: "POST",
176
+ headers: {
177
+ Authorization: `Bearer ${AUTH_TOKEN}`,
178
+ "Content-Type": "application/octet-stream",
179
+ },
180
+ body,
181
+ duplex: "half",
182
+ });
183
+ if (!res.ok)
184
+ return failFromResponse(res);
185
+ const data = await parseJsonResponse(res);
186
+ if (data.ok === false && typeof data.error === "string") {
187
+ return fail(String(data.error), typeof data.code === "string" ? data.code : undefined);
188
+ }
189
+ return ok({
190
+ ok: true,
191
+ local_path: localPath,
192
+ remote_path: remotePath,
193
+ size: stat.size,
194
+ });
195
+ }
196
+ catch (err) {
197
+ const error = err instanceof Error ? err.message : "unknown error";
198
+ return fail(error);
199
+ }
200
+ });
201
+ server.registerTool("download_file", {
202
+ description: "下载远程文件到本地;remote_path 为远程相对路径,local_path 为本地路径。",
203
+ inputSchema: z.object({
204
+ remote_path: z.string(),
205
+ local_path: z.string(),
206
+ overwrite: z.boolean().optional(),
207
+ mkdirs: z.boolean().optional(),
208
+ }),
209
+ outputSchema: z.union([
210
+ z.object({
211
+ ok: z.literal(true),
212
+ remote_path: z.string(),
213
+ local_path: z.string(),
214
+ size: z.number(),
215
+ }),
216
+ errorSchema,
217
+ ]),
218
+ }, async ({ remote_path: remotePath, local_path: localPath, overwrite, mkdirs, }) => {
219
+ try {
220
+ const localFull = resolveLocalPath(localPath);
221
+ if (mkdirs) {
222
+ await fs.mkdir(path.dirname(localFull), { recursive: true });
223
+ }
224
+ if (!overwrite) {
225
+ try {
226
+ await fs.stat(localFull);
227
+ return fail("file exists", "EEXIST");
228
+ }
229
+ catch (err) {
230
+ if (err.code !== "ENOENT")
231
+ throw err;
232
+ }
233
+ }
234
+ const url = buildUrl("/download", { path: remotePath });
235
+ const res = await fetchWithTimeout(url, {
236
+ method: "GET",
237
+ headers: { Authorization: `Bearer ${AUTH_TOKEN}` },
238
+ });
239
+ if (!res.ok)
240
+ return failFromResponse(res);
241
+ if (!res.body)
242
+ return fail("empty response body");
243
+ const stream = Readable.fromWeb(res.body);
244
+ await pipeline(stream, createWriteStream(localFull));
245
+ const stat = await fs.stat(localFull);
246
+ return ok({
247
+ ok: true,
248
+ local_path: localPath,
249
+ remote_path: remotePath,
250
+ size: stat.size,
251
+ });
252
+ }
253
+ catch (err) {
254
+ const error = err instanceof Error ? err.message : "unknown error";
255
+ return fail(error);
256
+ }
257
+ });
258
+ return server;
259
+ }
260
+ async function main() {
261
+ const server = createServer();
262
+ const transport = new StdioServerTransport();
263
+ await server.connect(transport);
264
+ }
265
+ main().catch((err) => {
266
+ console.error(err);
267
+ process.exit(1);
268
+ });
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "mcp-file-adapter",
3
+ "version": "0.1.0",
4
+ "description": "Local MCP stdio adapter for remote file service.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "bin": {
12
+ "mcp-file-adapter": "dist/index.js"
13
+ },
14
+ "scripts": {
15
+ "dev": "tsx src/index.ts",
16
+ "build": "tsc -p tsconfig.json",
17
+ "prepublishOnly": "npm run build",
18
+ "start": "node dist/index.js"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.25.3",
22
+ "zod": "^3.25.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^20.11.30",
26
+ "tsx": "^4.19.0",
27
+ "typescript": "^5.5.0"
28
+ }
29
+ }