mcp-cos-upload 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/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,74 @@
1
+ # mcp-cos-upload
2
+
3
+ MCP server for uploading files to Tencent Cloud COS. Supports uploading from URL (e.g., Figma export) or text content.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install mcp-cos-upload
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### MCP Configuration (Cursor / Claude Desktop)
14
+
15
+ Add to your MCP configuration:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "cos-upload": {
21
+ "command": "npx",
22
+ "args": ["-y", "mcp-cos-upload"],
23
+ "env": {
24
+ "COS_SECRET_ID": "your_secret_id",
25
+ "COS_SECRET_KEY": "your_secret_key",
26
+ "COS_DEFAULT_BUCKET": "your_bucket_name",
27
+ "COS_DEFAULT_REGION": "ap-guangzhou",
28
+ "COS_KEY_PREFIX": "uploads/",
29
+ "COS_CDN_DOMAIN": "your-cdn-domain.com"
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ### Environment Variables
37
+
38
+ | Variable | Required | Description |
39
+ |----------|----------|-------------|
40
+ | `COS_SECRET_ID` | ✅ | Tencent Cloud SecretId |
41
+ | `COS_SECRET_KEY` | ✅ | Tencent Cloud SecretKey |
42
+ | `COS_DEFAULT_BUCKET` | ✅ | Default COS bucket name |
43
+ | `COS_DEFAULT_REGION` | ✅ | Default COS region (e.g., `ap-guangzhou`) |
44
+ | `COS_KEY_PREFIX` | ❌ | Key prefix for uploaded files (e.g., `uploads/`) |
45
+ | `COS_CDN_DOMAIN` | ❌ | Custom CDN domain (e.g., `cdn.example.com`) |
46
+
47
+ ## Tools
48
+
49
+ ### cos_upload
50
+
51
+ Upload a file to Tencent Cloud COS.
52
+
53
+ **Parameters:**
54
+
55
+ | Parameter | Type | Required | Description |
56
+ |-----------|------|----------|-------------|
57
+ | `url` | string | ❌ | URL to download and upload (mutually exclusive with `content`) |
58
+ | `content` | string | ❌ | Text content to upload (mutually exclusive with `url`) |
59
+ | `bucket` | string | ❌ | COS bucket name (uses env default if not provided) |
60
+ | `region` | string | ❌ | COS region (uses env default if not provided) |
61
+ | `key` | string | ❌ | Object key (auto-generated if not provided) |
62
+ | `folder` | string | ❌ | Folder prefix (overrides `COS_KEY_PREFIX`) |
63
+ | `filename` | string | ❌ | Custom filename |
64
+ | `ext` | string | ❌ | File extension (e.g., `png`, `jpg`, `svg`) |
65
+
66
+ **Example:**
67
+
68
+ ```
69
+ Upload this image to COS: https://example.com/image.png
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "mcp-cos-upload",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for uploading files to Tencent Cloud COS",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": "./src/index.js",
8
+ "files": [
9
+ "src/**/*"
10
+ ],
11
+ "keywords": [
12
+ "mcp",
13
+ "cos",
14
+ "image-upload",
15
+ "figma"
16
+ ],
17
+ "author": "",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": ""
22
+ },
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "scripts": {
27
+ "start": "node src/index.js"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/sdk": "^1.12.0",
31
+ "cos-nodejs-sdk-v5": "^2.15.4",
32
+ "zod": "^3.24.0"
33
+ }
34
+ }
package/src/cos.js ADDED
@@ -0,0 +1,19 @@
1
+ // mcp-cos-upload - COS client configuration
2
+ import COS from "cos-nodejs-sdk-v5";
3
+
4
+ export function createCosClient() {
5
+ const secretId = process.env.COS_SECRET_ID;
6
+ const secretKey = process.env.COS_SECRET_KEY;
7
+
8
+ if (!secretId || !secretKey) {
9
+ console.error("[mcp-cos-upload] ❌ 缺少 COS_SECRET_ID 或 COS_SECRET_KEY");
10
+ process.exit(1);
11
+ }
12
+
13
+ return new COS({ SecretId: secretId, SecretKey: secretKey });
14
+ }
15
+
16
+ export const DEFAULT_BUCKET = process.env.COS_DEFAULT_BUCKET || null;
17
+ export const DEFAULT_REGION = process.env.COS_DEFAULT_REGION || null;
18
+ export const KEY_PREFIX = process.env.COS_KEY_PREFIX || "";
19
+ export const CDN_DOMAIN = process.env.COS_CDN_DOMAIN || null;
package/src/index.js ADDED
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ // mcp-cos-upload - MCP server for uploading files to Tencent Cloud COS
3
+
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ ListToolsRequestSchema,
8
+ CallToolRequestSchema,
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { z } from "zod";
11
+ import {
12
+ createCosClient,
13
+ DEFAULT_BUCKET,
14
+ DEFAULT_REGION,
15
+ KEY_PREFIX,
16
+ CDN_DOMAIN,
17
+ } from "./cos.js";
18
+
19
+ const cos = createCosClient();
20
+
21
+ // 工具参数校验(zod)
22
+ const CosUploadArgsSchema = z.object({
23
+ bucket: z.string().optional().describe("COS Bucket 名,不传则用默认 COS_DEFAULT_BUCKET"),
24
+ region: z.string().optional().describe("COS 区域,不传则用默认 COS_DEFAULT_REGION"),
25
+ key: z.string().optional().describe("对象 Key,不传则自动生成"),
26
+ folder: z.string().optional().describe("可选目录前缀(覆盖 env 中的 COS_KEY_PREFIX)"),
27
+ filename: z.string().optional().describe("可选文件名,例如使用 Figma 节点名称"),
28
+ ext: z.string().optional().describe("可选扩展名,如 png/jpg/svg"),
29
+ content: z.string().optional().describe("要上传的文本内容(和 url 二选一)"),
30
+ url: z.string().url().optional().describe("远程文件 URL(和 content 二选一,常用于 Figma 导出图片)"),
31
+ });
32
+
33
+ // MCP 客户端的 JSON Schema
34
+ const cosUploadInputSchema = {
35
+ type: "object",
36
+ properties: {
37
+ bucket: { type: "string", description: "COS Bucket 名,不传则用默认 COS_DEFAULT_BUCKET" },
38
+ region: { type: "string", description: "COS 区域,不传则用默认 COS_DEFAULT_REGION" },
39
+ key: { type: "string", description: "对象 Key,不传则自动生成" },
40
+ folder: { type: "string", description: "可选目录前缀(覆盖 env 中的 COS_KEY_PREFIX)" },
41
+ filename: { type: "string", description: "可选文件名,例如使用 Figma 节点名称" },
42
+ ext: { type: "string", description: "可选扩展名,如 png/jpg/svg,不传则从 filename/url 推断,默认 png" },
43
+ content: { type: "string", description: "要上传的文本内容(和 url 二选一)" },
44
+ url: { type: "string", description: "远程文件 URL(和 content 二选一,常用于 Figma 导出图片)" },
45
+ },
46
+ required: [],
47
+ };
48
+
49
+ async function main() {
50
+ const server = new Server(
51
+ { name: "mcp-cos-upload", version: "1.0.0" },
52
+ { capabilities: { tools: {} } }
53
+ );
54
+
55
+ // 列出可用工具
56
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
57
+ tools: [
58
+ {
59
+ name: "cos_upload",
60
+ description: "上传文件到腾讯云 COS。支持从 URL 下载上传或直接上传文本内容,返回 CDN 访问地址。",
61
+ inputSchema: cosUploadInputSchema,
62
+ },
63
+ ],
64
+ }));
65
+
66
+ // 处理工具调用
67
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
68
+ const { name, arguments: rawArgs } = request.params;
69
+
70
+ if (name !== "cos_upload") {
71
+ return { isError: true, content: [{ type: "text", text: `未知工具: ${name}` }] };
72
+ }
73
+
74
+ let args;
75
+ try {
76
+ args = CosUploadArgsSchema.parse(rawArgs || {});
77
+ } catch (err) {
78
+ return { isError: true, content: [{ type: "text", text: "参数校验失败: " + (err?.message || JSON.stringify(err)) }] };
79
+ }
80
+
81
+ let { bucket, region, key, folder, filename, ext, content, url } = args;
82
+
83
+ bucket = bucket || DEFAULT_BUCKET;
84
+ region = region || DEFAULT_REGION;
85
+
86
+ if (!bucket || !region) {
87
+ return { isError: true, content: [{ type: "text", text: "❌ bucket 和 region 必须指定。请在 env 配置 COS_DEFAULT_BUCKET / COS_DEFAULT_REGION,或在调用时传入。" }] };
88
+ }
89
+
90
+ if (!content && !url) {
91
+ return { isError: true, content: [{ type: "text", text: "必须提供 content 或 url 其中一个参数。" }] };
92
+ }
93
+
94
+ let body;
95
+ let contentType = null;
96
+ if (content) {
97
+ body = content;
98
+ } else {
99
+ const res = await fetch(url);
100
+ if (!res.ok) {
101
+ return { isError: true, content: [{ type: "text", text: `下载 URL 失败: ${res.status} ${res.statusText}` }] };
102
+ }
103
+ contentType = res.headers.get("content-type") || "";
104
+ body = Buffer.from(await res.arrayBuffer());
105
+ }
106
+
107
+ // 自动生成 key
108
+ if (!key) {
109
+ if (!ext) {
110
+ if (contentType) {
111
+ const contentTypeMap = {
112
+ "image/png": "png", "image/jpeg": "jpg", "image/jpg": "jpg",
113
+ "image/gif": "gif", "image/webp": "webp", "image/svg+xml": "svg",
114
+ "image/svg": "svg", "text/plain": "txt", "application/json": "json",
115
+ };
116
+ const mimeType = contentType.split(";")[0].trim().toLowerCase();
117
+ ext = contentTypeMap[mimeType];
118
+ }
119
+ if (!ext) {
120
+ const fromName = filename || (url ? new URL(url).pathname.split("/").pop() : "") || "";
121
+ const m = fromName.match(/\.([a-zA-Z0-9]+)$/);
122
+ ext = m ? m[1] : "png";
123
+ }
124
+ }
125
+
126
+ if (!filename) {
127
+ if (url) {
128
+ const last = new URL(url).pathname.split("/").pop() || "";
129
+ filename = last.replace(/\.[a-zA-Z0-9]+$/, "") || `img_${Date.now()}`;
130
+ } else {
131
+ filename = `img_${Date.now()}`;
132
+ }
133
+ }
134
+
135
+ const prefix = (folder || KEY_PREFIX || "").replace(/^\/+|\/+$/g, "");
136
+ const keyParts = prefix ? [prefix, `${filename}.${ext}`] : [`${filename}.${ext}`];
137
+ key = keyParts.join("/");
138
+ }
139
+
140
+ const start = Date.now();
141
+ const putObject = () => new Promise((resolve, reject) => {
142
+ cos.putObject({ Bucket: bucket, Region: region, Key: key, Body: body }, (err, data) => {
143
+ if (err) return reject(err);
144
+ resolve(data);
145
+ });
146
+ });
147
+
148
+ try {
149
+ const data = await putObject();
150
+ const elapsed = Date.now() - start;
151
+ const cdnUrl = CDN_DOMAIN
152
+ ? `https://${CDN_DOMAIN}/${encodeURI(key)}`
153
+ : `https://${bucket}.cos.${region}.myqcloud.com/${encodeURI(key)}`;
154
+
155
+ const summaryText = [
156
+ "COS 上传成功 ✅",
157
+ `Bucket: ${bucket}`,
158
+ `Region: ${region}`,
159
+ `Key: ${key}`,
160
+ `URL: ${cdnUrl}`,
161
+ `耗时: ${elapsed}ms`,
162
+ ].join("\n");
163
+
164
+ return {
165
+ content: [
166
+ { type: "text", text: summaryText },
167
+ { type: "text", text: JSON.stringify({ bucket, region, key, cdnUrl, raw: data }, null, 2) },
168
+ ],
169
+ };
170
+ } catch (err) {
171
+ return { isError: true, content: [{ type: "text", text: `COS 上传失败: ${err?.message || String(err)}` }] };
172
+ }
173
+ });
174
+
175
+ const transport = new StdioServerTransport();
176
+ await server.connect(transport);
177
+ }
178
+
179
+ main().catch((err) => {
180
+ console.error("[mcp-cos-upload] MCP server failed:", err);
181
+ process.exit(1);
182
+ });