asset-mcp 0.2.1 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -66
- package/package.json +5 -2
- package/src/asset-bundle.js +52 -0
- package/src/backend.js +21 -0
- package/src/config.js +2 -0
- package/src/download-asset.js +168 -0
- package/src/server.js +79 -10
package/README.md
CHANGED
|
@@ -1,94 +1,63 @@
|
|
|
1
1
|
# asset-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
在 Cursor 等支持 MCP 的 IDE 中连接资产管理平台(OAuth2 设备码登录;bundle 打 zip 依赖 `jszip`)。
|
|
4
4
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- `mcp_auth`:`methods` / `start` / `poll` / `status` / `logout`(OAuth2 设备码)
|
|
8
|
+
- `auth_me`:当前用户
|
|
9
|
+
- `assets_search`:查询资产列表
|
|
10
|
+
- `assets_detail`:按 ID 查单条详情(须指定类型;能否查看由后端判定)
|
|
11
|
+
- `assets_download`:按 ID 将资产写入**本工程工作区**(须配置 `ASSET_MCP_WORKSPACE_ROOT`;鉴权同详情)
|
|
12
|
+
- `assets_create`:创建资产(权限由后端拦截)
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## 使用
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
|
-
|
|
17
|
-
npm start
|
|
17
|
+
npx -y asset-mcp@latest
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npx -y asset-mcp
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## 发布到 npm
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm login
|
|
30
|
-
npm publish --access public
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
> 如果包名已被占用,请先修改 `package.json` 里的 `name`(例如加组织作用域)。
|
|
34
|
-
|
|
35
|
-
## 在 Cursor 中配置(示例)
|
|
36
|
-
|
|
37
|
-
将 MCP 命令配置为:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
npx -y asset-mcp
|
|
41
|
-
```
|
|
20
|
+
在 Cursor 的 MCP 配置里将启动命令设为上述命令,并配置下方环境变量。
|
|
42
21
|
|
|
43
22
|
## 环境变量
|
|
44
23
|
|
|
45
24
|
```bash
|
|
46
25
|
export ASSET_MCP_BASE_URL="http://localhost:8080"
|
|
47
|
-
|
|
48
|
-
export
|
|
26
|
+
# 下载到本工程目录时必填(Cursor MCP 里可用 ${workspaceFolder})
|
|
27
|
+
export ASSET_MCP_WORKSPACE_ROOT="/你的工程根路径"
|
|
49
28
|
```
|
|
50
29
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- `ASSET_MCP_DEVICE_CODE_PATH`(默认 `/oauth/device/code`)
|
|
54
|
-
- `ASSET_MCP_TOKEN_PATH`(默认 `/oauth/token`)
|
|
55
|
-
- `ASSET_MCP_REVOKE_PATH`(默认 `/oauth/revoke`)
|
|
56
|
-
|
|
57
|
-
## 标准授权流程(Device Code)
|
|
30
|
+
## 登录(设备码)
|
|
58
31
|
|
|
59
|
-
1.
|
|
60
|
-
2.
|
|
61
|
-
3.
|
|
62
|
-
4.
|
|
63
|
-
5. 然后可调用 `assets.create`
|
|
32
|
+
1. 调用 `mcp_auth`,`{ "action": "start" }`
|
|
33
|
+
2. 在浏览器打开返回的 `verificationUri`,按提示完成授权(可配合 `userCode`)
|
|
34
|
+
3. 调用 `mcp_auth`,`{ "action": "poll" }`
|
|
35
|
+
4. 使用 `auth_me` 确认已登录
|
|
64
36
|
|
|
65
|
-
|
|
37
|
+
可先调用 `{ "action": "methods" }` 查看说明。请在浏览器中登录,**不要**在对话里向 MCP 发送密码。
|
|
66
38
|
|
|
67
|
-
|
|
39
|
+
## `assets_search`
|
|
68
40
|
|
|
69
|
-
|
|
41
|
+
- `assetType`:`rule` | `skill` | `mcp` | `all`(默认 `all`)
|
|
42
|
+
- `keyword`:关键词,透传后端,与平台搜索一致;固定第 1 页,每类最多 10 条;`all` 时每类各最多 10 条
|
|
70
43
|
|
|
71
|
-
|
|
72
|
-
- `keyword`: 关键词
|
|
44
|
+
## `assets_detail`
|
|
73
45
|
|
|
74
|
-
|
|
46
|
+
- `assetType`:`rule` | `skill` | `mcp`(必填)
|
|
47
|
+
- `id`:资产 ID(必填,正整数)
|
|
75
48
|
|
|
76
|
-
|
|
77
|
-
- `keyword` 直接透传平台后端,因此搜索逻辑与平台当前页面一致
|
|
78
|
-
- 不开放分页参数,固定查询第 1 页、每类最多 10 条
|
|
79
|
-
- `assetType=all` 时:`rule/skill/mcp` 各返回最多 10 条
|
|
49
|
+
无查看权限或资源不存在时,接口会失败;与 Web 端一致,由后端统一鉴权。
|
|
80
50
|
|
|
81
|
-
##
|
|
51
|
+
## `assets_download`
|
|
82
52
|
|
|
83
|
-
- `assetType
|
|
84
|
-
- `
|
|
85
|
-
- `
|
|
86
|
-
-
|
|
87
|
-
- `
|
|
88
|
-
- `content`: 可选(rule/skill)
|
|
89
|
-
- `mcpType/configJson/toolsJson/remark`: 可选(mcp)
|
|
53
|
+
- `assetType`、`id`:同 `assets_detail`(无权限则无法拉取内容)
|
|
54
|
+
- `asZip`:可选,**rule/skill 且为 bundle** 时设为 `true` 则只生成一个 `.zip`,不写展开目录
|
|
55
|
+
- `relativeDir`:可选,表示 **`.cursor` 下面的子路径**(不要带 `.cursor` 前缀);默认空,即直接 `{工作区}/.cursor/rules|skills|mcps/{资产名}/`
|
|
56
|
+
- **rule / skill**:无 ASSET_BUNDLE 时为单个 `.md`;有 bundle 时默认展开为多文件目录;传 **`asZip: true`** 时为单个 `.zip`(与 Web 端 zip 一致,例:`.cursor/rules/测试.zip`)
|
|
57
|
+
- **mcp**:写入 `config.json`、`tools.json`、`meta.md`
|
|
90
58
|
|
|
91
|
-
##
|
|
59
|
+
## `assets_create`
|
|
92
60
|
|
|
93
|
-
-
|
|
94
|
-
-
|
|
61
|
+
- `assetType`:`rule` | `skill` | `mcp`
|
|
62
|
+
- `name`、`description`:必填(按后端校验)
|
|
63
|
+
- `version`、`changeLog`、`content`(rule/skill)、`mcpType` / `configJson` / `toolsJson` / `remark`(mcp)等见平台接口约定
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asset-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "MCP server for asset operations in Cursor",
|
|
6
6
|
"type": "module",
|
|
@@ -16,12 +16,15 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"start": "node src/server.js",
|
|
19
|
-
"check": "node --check src/server.js",
|
|
19
|
+
"check": "node --check src/server.js && node --check src/backend.js && node --check src/download-asset.js && node --check src/asset-bundle.js",
|
|
20
20
|
"prepublishOnly": "npm run check"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"registry": "https://registry.npmjs.org/"
|
|
24
24
|
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"jszip": "^3.10.1"
|
|
27
|
+
},
|
|
25
28
|
"engines": {
|
|
26
29
|
"node": ">=18"
|
|
27
30
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 与前端 assetBundle.ts 对齐:解析正文中的 ASSET_BUNDLE_V1 块。
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const MARKER_REGEX = /<!--\s*ASSET_BUNDLE_V1:([A-Za-z0-9+/=]+)\s*-->/
|
|
6
|
+
|
|
7
|
+
export function parseAssetBundle(raw, defaultEntryPath = 'ENTRY') {
|
|
8
|
+
const source = raw ?? ''
|
|
9
|
+
const match = source.match(MARKER_REGEX)
|
|
10
|
+
if (!match) {
|
|
11
|
+
return {
|
|
12
|
+
entryContent: source,
|
|
13
|
+
files: [],
|
|
14
|
+
folders: [],
|
|
15
|
+
entryPath: defaultEntryPath,
|
|
16
|
+
bundled: false,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const marker = match[0]
|
|
21
|
+
const entryContent = source.replace(marker, '').trimEnd()
|
|
22
|
+
try {
|
|
23
|
+
const decoded = Buffer.from(match[1], 'base64').toString('utf8')
|
|
24
|
+
const payload = JSON.parse(decoded)
|
|
25
|
+
const files = Array.isArray(payload.files)
|
|
26
|
+
? payload.files
|
|
27
|
+
.filter(f => f && typeof f.path === 'string')
|
|
28
|
+
.map(f => ({ path: f.path, content: String(f.content ?? '') }))
|
|
29
|
+
: []
|
|
30
|
+
const folders = Array.isArray(payload.folders)
|
|
31
|
+
? payload.folders.filter(f => typeof f === 'string').map(f => f.trim()).filter(Boolean)
|
|
32
|
+
: []
|
|
33
|
+
if (!files.length) {
|
|
34
|
+
return { entryContent, files: [], folders, entryPath: defaultEntryPath, bundled: false }
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
entryContent,
|
|
38
|
+
files,
|
|
39
|
+
folders,
|
|
40
|
+
entryPath: payload.entryPath || files[0].path || defaultEntryPath,
|
|
41
|
+
bundled: true,
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
return {
|
|
45
|
+
entryContent,
|
|
46
|
+
files: [],
|
|
47
|
+
folders: [],
|
|
48
|
+
entryPath: defaultEntryPath,
|
|
49
|
+
bundled: false,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/backend.js
CHANGED
|
@@ -88,6 +88,27 @@ export async function whoAmI() {
|
|
|
88
88
|
return requestWithToken(config.api.mePath, { method: 'GET' })
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function parsePositiveId(raw) {
|
|
92
|
+
const n = typeof raw === 'string' ? parseInt(raw, 10) : Number(raw)
|
|
93
|
+
if (!Number.isFinite(n) || n < 1 || !Number.isInteger(n)) {
|
|
94
|
+
throw new Error('id 须为正整数')
|
|
95
|
+
}
|
|
96
|
+
return n
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 单条详情:走平台 GET /api/v1/assets/{type}/{id},可见性与角色由后端 SQL + 业务校验,无权限时 404/业务错误。
|
|
101
|
+
*/
|
|
102
|
+
export async function getAssetDetail(args = {}) {
|
|
103
|
+
const assetType = args.assetType
|
|
104
|
+
if (!['rule', 'skill', 'mcp'].includes(assetType)) {
|
|
105
|
+
throw new Error('assetType 须为 rule、skill 或 mcp')
|
|
106
|
+
}
|
|
107
|
+
const id = parsePositiveId(args.id)
|
|
108
|
+
const path = config.api.assetDetailPath(assetType, id)
|
|
109
|
+
return requestWithToken(path, { method: 'GET' })
|
|
110
|
+
}
|
|
111
|
+
|
|
91
112
|
export async function searchAssets(args = {}) {
|
|
92
113
|
const assetType = args.assetType || 'all'
|
|
93
114
|
const query = buildAssetListQuery(args)
|
package/src/config.js
CHANGED
|
@@ -26,6 +26,8 @@ export const config = {
|
|
|
26
26
|
authLogoutPath: '/api/v1/auth/logout',
|
|
27
27
|
/** 统一资产:GET 列表 / POST 创建,路径变量为 rule | skill | mcp */
|
|
28
28
|
assetsPath: (assetType) => `/api/v1/assets/${assetType}`,
|
|
29
|
+
/** 单条详情:GET,权限由后端校验 */
|
|
30
|
+
assetDetailPath: (assetType, id) => `/api/v1/assets/${assetType}/${id}`,
|
|
29
31
|
/** 跨类型列表(query 可带 assetType / keyword / mine 等) */
|
|
30
32
|
assetsMarketPath: '/api/v1/assets',
|
|
31
33
|
mePath: '/api/v1/auth/me',
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import JSZip from 'jszip'
|
|
4
|
+
import { parseAssetBundle } from './asset-bundle.js'
|
|
5
|
+
|
|
6
|
+
function sanitizeFilename(name, fallback) {
|
|
7
|
+
const normalized = String(name ?? '').trim().replace(/[\\/:*?"<>|]+/g, '-')
|
|
8
|
+
return normalized || fallback
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeExportPath(filePath, entryPath) {
|
|
12
|
+
if (filePath !== entryPath) return filePath
|
|
13
|
+
const slashIdx = filePath.lastIndexOf('/')
|
|
14
|
+
const dotIdx = filePath.lastIndexOf('.')
|
|
15
|
+
const hasExt = dotIdx > slashIdx
|
|
16
|
+
return hasExt ? filePath : `${filePath}.md`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assertUnderRoot(rootDir, absPath) {
|
|
20
|
+
const root = path.resolve(rootDir)
|
|
21
|
+
const abs = path.resolve(absPath)
|
|
22
|
+
const rel = path.relative(root, abs)
|
|
23
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
24
|
+
throw new Error('路径超出 ASSET_MCP_WORKSPACE_ROOT,拒绝写入')
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 将相对路径拆成安全段,禁止 .. */
|
|
29
|
+
function splitSafeRel(rel) {
|
|
30
|
+
return String(rel ?? '')
|
|
31
|
+
.split(/[/\\]+/)
|
|
32
|
+
.map(s => s.trim())
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.map(seg => {
|
|
35
|
+
if (seg === '..' || seg === '.') throw new Error('非法相对路径')
|
|
36
|
+
if (seg.includes('/') || seg.includes('\\')) throw new Error('非法路径段')
|
|
37
|
+
return seg
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveUnderRoot(rootDir, ...segments) {
|
|
42
|
+
let cur = path.resolve(rootDir)
|
|
43
|
+
for (const seg of segments) {
|
|
44
|
+
if (!seg) continue
|
|
45
|
+
cur = path.join(cur, seg)
|
|
46
|
+
assertUnderRoot(rootDir, cur)
|
|
47
|
+
}
|
|
48
|
+
return cur
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildMcpMeta(asset) {
|
|
52
|
+
const lines = [
|
|
53
|
+
`# ${asset.name ?? 'MCP'}`,
|
|
54
|
+
'',
|
|
55
|
+
asset.description ? `## 简介\n\n${asset.description}\n` : '',
|
|
56
|
+
asset.remark ? `## 备注\n\n${asset.remark}\n` : '',
|
|
57
|
+
`版本: ${asset.version ?? '-'}`,
|
|
58
|
+
`类型: ${asset.type ?? '-'}`,
|
|
59
|
+
]
|
|
60
|
+
return lines.filter(Boolean).join('\n')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function safeFileRel(rel) {
|
|
64
|
+
return String(rel)
|
|
65
|
+
.split(/[/\\]+/)
|
|
66
|
+
.map(s => s.trim())
|
|
67
|
+
.filter(Boolean)
|
|
68
|
+
.map(seg => {
|
|
69
|
+
if (seg === '..' || seg === '.') throw new Error('非法文件路径')
|
|
70
|
+
return seg
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 与工程习惯一致:规则 → rules,技能 → skills,MCP → mcps */
|
|
75
|
+
function typeFolder(assetType) {
|
|
76
|
+
if (assetType === 'rule') return 'rules'
|
|
77
|
+
if (assetType === 'skill') return 'skills'
|
|
78
|
+
if (assetType === 'mcp') return 'mcps'
|
|
79
|
+
throw new Error(`不支持的 assetType: ${assetType}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 将资产详情写入工作区目录(与前端导出语义接近)。
|
|
84
|
+
* @param {{ asZip?: boolean }} [options] rule/skill 且为 bundle 时,asZip=true 生成单个 zip(与 Web 端 zip 导出一致)
|
|
85
|
+
* @returns {{ paths: string[], rootDir: string, format: string }}
|
|
86
|
+
*/
|
|
87
|
+
export async function exportAssetToWorkspace(asset, workspaceRoot, relativeDir, options = {}) {
|
|
88
|
+
const asZip = Boolean(options.asZip)
|
|
89
|
+
const root = path.resolve(workspaceRoot.trim())
|
|
90
|
+
assertUnderRoot(root, root)
|
|
91
|
+
|
|
92
|
+
/** 固定落在工作区 `.cursor/` 下;relativeDir 为其下的可选子目录(多级用 /) */
|
|
93
|
+
let dirParts = splitSafeRel(relativeDir ?? '')
|
|
94
|
+
|
|
95
|
+
const assetType = asset.assetType
|
|
96
|
+
const baseName = sanitizeFilename(asset.name, `${assetType}-${asset.id}`)
|
|
97
|
+
const tf = typeFolder(assetType)
|
|
98
|
+
const destRoot = resolveUnderRoot(root, '.cursor', ...dirParts, tf, baseName)
|
|
99
|
+
|
|
100
|
+
if (assetType === 'rule' || assetType === 'skill') {
|
|
101
|
+
const defaultEntry = assetType === 'rule' ? 'RULE' : 'SKILL'
|
|
102
|
+
const content = asset.content ?? ''
|
|
103
|
+
const parsed = parseAssetBundle(content, defaultEntry)
|
|
104
|
+
|
|
105
|
+
if (parsed.bundled && asZip) {
|
|
106
|
+
const zip = new JSZip()
|
|
107
|
+
for (const folder of parsed.folders) {
|
|
108
|
+
if (folder.trim()) zip.folder(folder)
|
|
109
|
+
}
|
|
110
|
+
for (const file of parsed.files) {
|
|
111
|
+
if (!file.path.trim()) continue
|
|
112
|
+
const rel = normalizeExportPath(file.path, parsed.entryPath)
|
|
113
|
+
zip.file(rel, file.content ?? '')
|
|
114
|
+
}
|
|
115
|
+
const buf = await zip.generateAsync({ type: 'nodebuffer' })
|
|
116
|
+
const zipPath = resolveUnderRoot(root, '.cursor', ...dirParts, tf, `${baseName}.zip`)
|
|
117
|
+
await fs.mkdir(path.dirname(zipPath), { recursive: true })
|
|
118
|
+
await fs.writeFile(zipPath, buf)
|
|
119
|
+
const parent = path.dirname(zipPath)
|
|
120
|
+
return { paths: [zipPath], rootDir: parent, format: 'bundle-zip' }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await fs.mkdir(destRoot, { recursive: true })
|
|
124
|
+
|
|
125
|
+
if (!parsed.bundled) {
|
|
126
|
+
const filePath = resolveUnderRoot(root, '.cursor', ...dirParts, tf, baseName, `${baseName}.md`)
|
|
127
|
+
await fs.writeFile(filePath, content, 'utf8')
|
|
128
|
+
return { paths: [filePath], rootDir: destRoot, format: 'single-markdown' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const folder of parsed.folders) {
|
|
132
|
+
if (!folder.trim()) continue
|
|
133
|
+
const parts = safeFileRel(folder)
|
|
134
|
+
const dir = resolveUnderRoot(root, '.cursor', ...dirParts, tf, baseName, ...parts)
|
|
135
|
+
await fs.mkdir(dir, { recursive: true })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const written = []
|
|
139
|
+
for (const file of parsed.files) {
|
|
140
|
+
if (!file.path.trim()) continue
|
|
141
|
+
const rel = normalizeExportPath(file.path, parsed.entryPath)
|
|
142
|
+
const fparts = safeFileRel(rel)
|
|
143
|
+
const filePath = resolveUnderRoot(root, '.cursor', ...dirParts, tf, baseName, ...fparts)
|
|
144
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
145
|
+
await fs.writeFile(filePath, file.content ?? '', 'utf8')
|
|
146
|
+
written.push(filePath)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { paths: written, rootDir: destRoot, format: 'bundle' }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (assetType === 'mcp') {
|
|
153
|
+
await fs.mkdir(destRoot, { recursive: true })
|
|
154
|
+
const paths = []
|
|
155
|
+
const p1 = resolveUnderRoot(root, '.cursor', ...dirParts, tf, baseName, 'config.json')
|
|
156
|
+
const p2 = resolveUnderRoot(root, '.cursor', ...dirParts, tf, baseName, 'tools.json')
|
|
157
|
+
const p3 = resolveUnderRoot(root, '.cursor', ...dirParts, tf, baseName, 'meta.md')
|
|
158
|
+
await fs.writeFile(p1, asset.configJson ?? '{}', 'utf8')
|
|
159
|
+
paths.push(p1)
|
|
160
|
+
await fs.writeFile(p2, asset.toolsJson ?? '[]', 'utf8')
|
|
161
|
+
paths.push(p2)
|
|
162
|
+
await fs.writeFile(p3, buildMcpMeta(asset), 'utf8')
|
|
163
|
+
paths.push(p3)
|
|
164
|
+
return { paths, rootDir: destRoot, format: 'mcp-json' }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw new Error(`不支持的 assetType: ${assetType}`)
|
|
168
|
+
}
|
package/src/server.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import readline from 'node:readline'
|
|
2
|
-
import { createAsset, searchAssets, whoAmI } from './backend.js'
|
|
2
|
+
import { createAsset, getAssetDetail, searchAssets, whoAmI } from './backend.js'
|
|
3
|
+
import { exportAssetToWorkspace } from './download-asset.js'
|
|
3
4
|
import { readTokens } from './token-store.js'
|
|
4
5
|
import { pollDeviceToken, revokeAndClearTokens, startDeviceAuthorization } from './oauth.js'
|
|
5
6
|
|
|
@@ -23,7 +24,7 @@ const TOOL_DEFS = [
|
|
|
23
24
|
},
|
|
24
25
|
},
|
|
25
26
|
{
|
|
26
|
-
name: '
|
|
27
|
+
name: 'auth_me',
|
|
27
28
|
description: '查看当前登录用户信息',
|
|
28
29
|
inputSchema: {
|
|
29
30
|
type: 'object',
|
|
@@ -32,9 +33,9 @@ const TOOL_DEFS = [
|
|
|
32
33
|
},
|
|
33
34
|
},
|
|
34
35
|
{
|
|
35
|
-
name: '
|
|
36
|
+
name: 'assets_search',
|
|
36
37
|
description:
|
|
37
|
-
'查询资产(rule/skill/mcp/all),GET /api/v1/assets/{assetType};固定每类最多 10 条(第 1
|
|
38
|
+
'查询资产(rule/skill/mcp/all),GET /api/v1/assets/{assetType};固定每类最多 10 条(第 1 页),无分页参数。若用户只说「查询资产」等而未给出筛选条件,助手应先询问需要哪些条件,并列出可选:assetType、keyword、mine、status、categoryId/categoryIds、type(仅 mcp:local/remote);用户补充或确认后再调用。',
|
|
38
39
|
inputSchema: {
|
|
39
40
|
type: 'object',
|
|
40
41
|
properties: {
|
|
@@ -50,7 +51,42 @@ const TOOL_DEFS = [
|
|
|
50
51
|
},
|
|
51
52
|
},
|
|
52
53
|
{
|
|
53
|
-
name: '
|
|
54
|
+
name: 'assets_detail',
|
|
55
|
+
description:
|
|
56
|
+
'按 ID 查询单条资产详情(须指定 assetType)。能否查看由后端按当前用户、资产状态与可见范围判定;无权限或不存在时返回错误。MCP 仅转发请求,无法在客户端绕过权限。',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
assetType: { type: 'string', enum: ['rule', 'skill', 'mcp'] },
|
|
61
|
+
id: { type: 'integer', minimum: 1, description: '资产主键 ID' },
|
|
62
|
+
},
|
|
63
|
+
required: ['assetType', 'id'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'assets_download',
|
|
68
|
+
description:
|
|
69
|
+
'按 ID 将资产下载到本机工作区 .cursor 目录下:先走与详情相同的接口鉴权,再写入文件。须设置 ASSET_MCP_WORKSPACE_ROOT。路径为 .cursor/{可选子目录}/rules|skills|mcps/。rule/skill:无 bundle 为单 .md;有 bundle 时默认展开为文件夹,若 asZip=true 则打成一个 .zip(与 Web 导出 zip 一致);mcp 为 config.json、tools.json、meta.md。',
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
assetType: { type: 'string', enum: ['rule', 'skill', 'mcp'] },
|
|
74
|
+
id: { type: 'integer', minimum: 1, description: '资产主键 ID' },
|
|
75
|
+
relativeDir: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: '位于 .cursor 下的可选子目录,如 imported 或 backup/2026(不要写 .cursor 前缀),默认空即直接写在 .cursor/rules 等下',
|
|
78
|
+
},
|
|
79
|
+
asZip: {
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
description:
|
|
82
|
+
'仅 rule/skill 且正文为 ASSET_BUNDLE 时有效:true 时生成单个 .zip(如 .cursor/rules/名称.zip),false 为展开目录(默认)',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ['assetType', 'id'],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'assets_create',
|
|
54
90
|
description: '创建资产(权限最终由后端拦截)',
|
|
55
91
|
inputSchema: {
|
|
56
92
|
type: 'object',
|
|
@@ -103,7 +139,7 @@ function authMethodsPayload() {
|
|
|
103
139
|
'① mcp_auth action=start → 取得 userCode、verificationUri(或 verificationUriComplete)',
|
|
104
140
|
'② 用户在浏览器打开链接,完成授权(若页面要求输入用户码,使用返回的 userCode)',
|
|
105
141
|
'③ mcp_auth action=poll → 换取 access_token 并写入本地凭证',
|
|
106
|
-
'④ 可选:
|
|
142
|
+
'④ 可选:auth_me 或 mcp_auth action=status 校验',
|
|
107
143
|
],
|
|
108
144
|
hint: '用户可说「我要登录资产管理平台」。勿使用账号密码调用 MCP;账号密码仅在浏览器授权页由用户自行输入。',
|
|
109
145
|
}
|
|
@@ -210,11 +246,44 @@ async function handleAssetsSearch(args = {}) {
|
|
|
210
246
|
})
|
|
211
247
|
}
|
|
212
248
|
|
|
249
|
+
async function handleAssetsDetail(args = {}) {
|
|
250
|
+
const data = await getAssetDetail(args)
|
|
251
|
+
return makeTextResult({
|
|
252
|
+
ok: true,
|
|
253
|
+
message: '查询成功',
|
|
254
|
+
data,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function handleAssetsDownload(args = {}) {
|
|
259
|
+
const workspaceRoot = process.env.ASSET_MCP_WORKSPACE_ROOT?.trim()
|
|
260
|
+
if (!workspaceRoot) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
'未设置 ASSET_MCP_WORKSPACE_ROOT。请在 MCP 的 env 中配置为当前工程根目录,例如:ASSET_MCP_WORKSPACE_ROOT = ${workspaceFolder}(Cursor 会展开变量)。'
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
const asset = await getAssetDetail(args)
|
|
266
|
+
const merged = { ...asset, assetType: asset.assetType ?? args.assetType }
|
|
267
|
+
const out = await exportAssetToWorkspace(merged, workspaceRoot, args.relativeDir, {
|
|
268
|
+
asZip: Boolean(args.asZip),
|
|
269
|
+
})
|
|
270
|
+
return makeTextResult({
|
|
271
|
+
ok: true,
|
|
272
|
+
message: '已写入工作区',
|
|
273
|
+
workspaceRoot,
|
|
274
|
+
relativeDir: args.relativeDir ?? '',
|
|
275
|
+
underCursor: true,
|
|
276
|
+
...out,
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
213
280
|
async function handleToolCall(name, args) {
|
|
214
281
|
if (name === 'mcp_auth') return handleAuthTool(args)
|
|
215
|
-
if (name === 'auth.me') return makeTextResult(await whoAmI())
|
|
216
|
-
if (name === 'assets.search') return handleAssetsSearch(args)
|
|
217
|
-
if (name === 'assets.
|
|
282
|
+
if (name === 'auth_me' || name === 'auth.me') return makeTextResult(await whoAmI())
|
|
283
|
+
if (name === 'assets_search' || name === 'assets.search') return handleAssetsSearch(args)
|
|
284
|
+
if (name === 'assets_detail' || name === 'assets.detail') return handleAssetsDetail(args)
|
|
285
|
+
if (name === 'assets_download' || name === 'assets.download') return handleAssetsDownload(args)
|
|
286
|
+
if (name === 'assets_create' || name === 'assets.create') return handleAssetsCreate(args)
|
|
218
287
|
throw new Error(`未知工具: ${name}`)
|
|
219
288
|
}
|
|
220
289
|
|
|
@@ -225,7 +294,7 @@ async function handleRequest(req) {
|
|
|
225
294
|
return ok(id, {
|
|
226
295
|
protocolVersion: '2024-11-05',
|
|
227
296
|
capabilities: { tools: {} },
|
|
228
|
-
serverInfo: { name: 'asset-mcp', version: '0.2.
|
|
297
|
+
serverInfo: { name: 'asset-mcp', version: '0.2.9' },
|
|
229
298
|
})
|
|
230
299
|
}
|
|
231
300
|
|