@co-villa/agentnest-tools 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,61 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @co-villa/agentnest-tools:MCP 与 CLI
2
+
3
+ ## Agent 必须自动完成的本地开发流程
4
+
5
+ 默认复用用户已授权的 MCP 连接。Agent 不得让用户手动填写平台 URL、AGENTDOCK_TOKEN 或资源令牌:
6
+
7
+ 1. 调用 list_projects 确认目标 projectId,并检查该项目资源。
8
+ 2. 本地启动 App 前自动调用 prepare_local_development({ projectId });本地 stdio MCP 同时传 App 根目录 directory。
9
+ 3. 远程 MCP 返回 config 时,由 Agent 自动保存为 App 根目录 .agentdock/development.json,并把 .agentdock/ 加入 .gitignore;stdio 工具自动执行此步骤。不要让用户复制工具响应或创建绑定文件。
10
+ 4. 安装平台最新 SDK,App 服务端代码默认使用 createClient(),不填 url/projectId/token。
11
+ 5. 自动运行并验证本地 App 能访问数据库、KV 和 OSS。绑定缺失、过期或项目切换时由 Agent 重新调用准备工具;若 MCP 授权失效,仅请用户重新完成项目授权,再自动继续准备。
12
+
13
+ 数据库创建后需等待 ready;SQL 和写入需要 deploy 权限。不要把 .agentdock/development.json 提交、部署或暴露给浏览器,不复制原 MCP OAuth 凭据。开发绑定最长24小时,资源访问配置10分钟由 SDK 自动续期。
14
+
15
+ 推荐通过 OAuth 连接远程 MCP: https://an.co-villa.cn/mcp 。本地 stdio 入口为 agentnest-mcp,通过 AGENTDOCK_URL 与 AGENTDOCK_TOKEN 配置 Agent 的部署身份。
16
+
17
+ Streamable HTTP 使用 /mcp;旧式 SSE 客户端(如百度搭子的官方配置示例)使用 https://an.co-villa.cn/sse,认证请求头为 Authorization: Bearer <Agent令牌>。千问等入口按其支持的协议选择地址;火山方舟 Remote MCP 使用 /mcp 和 tools.headers.Authorization。每种传输共用权限与工具,旧式 SSE 的每次 POST 验证原授权。控制台可生成账号范围的 read/deploy/manage 令牌;scope manage 可创建和配置项目。支持 OAuth 的客户端仍优先使用浏览器授权,不需要手动令牌。
18
+
19
+ OAuth 可选择账号范围(包含未来有权访问的项目)或单个项目。账号范围的 manage 授权允许 create_project、update_project、set_project_environment、set_project_scaling 以及资源和部署管理;get_project 可检查配置及环境变量名称。read 和 deploy 不允许创建项目或修改所有者配置。操作按当前项目角色和配额校验,账号密码、用户管理和令牌签发仍需用户完整登录身份。旧授权需重新授权才可获取 manage 权限。
20
+
21
+ ## Agent 开发 Node.js App
22
+
23
+ Node.js App 默认启用项目资源绑定。开发服务端代码时安装 @co-villa/agentnest-sdk 并使用 createClient(),平台自动注入访问配置;不要让用户手动生成资源令牌,不要将部署令牌作为应用资源令牌。资源需先通过 MCP 创建,数据库轮询到 ready;resources:false 可显式关闭绑定。
24
+
25
+ 用 list_resources 检查项目资源;用 create_database、create_kv_namespace、create_storage_bucket 创建缺失资源,用 get_database 轮询 ready。开发阶段可使用 query_database、get_kv_entry、put_kv_entry、put_storage_object 等工具操作数据。
26
+
27
+ 安装最新 @co-villa/agentnest-sdk 并保留 lockfile,在 App 服务端代码中使用:
28
+
29
+ ```js
30
+ import { createClient } from '@co-villa/agentnest-sdk';
31
+ const app = createClient();
32
+ const config = await app.kv('settings').get('config');
33
+ const { rows } = await app.db.query('SELECT $1::text AS message', ['hello']);
34
+ const files = await app.oss('uploads').list();
35
+ ```
36
+
37
+ 通过 deploy_site 或 CLI 部署 Node.js App,无需显式设置 resources:true。resourceScopes 可指定权限子集;需要关闭绑定时使用 resources:false,CLI 使用 --no-resources。等待部署 ready 后检查网站。Node.js 须监听 process.env.PORT 和 0.0.0.0。
38
+
39
+ App 自动资源令牌与 Agent 部署令牌用途不同,不要混用;不需要给 App 生成手动令牌。纯静态浏览器通过 App 后端访问资源;Worker 显式开启 resources:true 后使用 env.KV、env.DB、env.OSS。
40
+
41
+ 数据库密码与删除数据库/命名空间/桶、域名变更仍要求所有者完整身份;普通项目令牌受项目隔离和配额约束。完整工具说明见平台 /help/mcp 与仓库 docs/AGENT_DEPLOY.md。
42
+
43
+ ## 本地开发自动复用 MCP 授权
44
+
45
+ 用户只需在 Agent 客户端连接 MCP 并授权目标项目,不必设置 AGENTDOCK_URL/AGENTDOCK_TOKEN,也不必生成资源令牌。
46
+
47
+ Agent 开发本地 App 时,自动调用 `prepare_local_development({ projectId })`。远程 MCP 返回仅限该项目的开发绑定,Agent 自动将 config 写到 App 根目录 `.agentdock/development.json`,并将 `.agentdock/` 加入 .gitignore;不要让用户复制凭据或填写环境变量。本地 stdio MCP 可传 directory,工具会自动完成保存和 git 忽略。
48
+
49
+ App 中直接使用:
50
+
51
+ ```js
52
+ import { createClient } from '@co-villa/agentnest-sdk';
53
+ const app = createClient();
54
+ const config = await app.kv('settings').get('config');
55
+ const { rows } = await app.db.query('SELECT $1::text AS message', ['hello']);
56
+ const files = await app.oss('uploads').list();
57
+ ```
58
+
59
+ SDK 自动读取本地绑定、换取 10 分钟有效的资源配置,并在过期前续期。开发绑定最长 24 小时,不保存 MCP 原始 OAuth/Agent 令牌;原 MCP 授权被撤销、过期或失去项目权限时立即拒绝资源访问。绑定过期时由 Agent 重新调用准备工具。使用其他项目时由 Agent 重新准备绑定;createClient({ project }) 会校验匹配,不会擅自切换项目。
60
+
61
+ read 授权仅允许读取 KV/OSS,SQL 与写入需要 deploy 授权。数据库、命名空间和桶必须已创建。本地访问真实项目资源,浏览器通过本地 Node.js 后端访问。绑定文件不提交、不上传,CLI 自动排除 .agentdock 目录。已有环境变量授权方式仅作为兼容路径,不再是默认开发指引。
package/cli/index.mjs ADDED
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, readdir, lstat, writeFile } from 'node:fs/promises';
3
+ import { resolve, join, relative, sep } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { realpathSync } from 'node:fs';
7
+
8
+ export async function collectFiles(directory) {
9
+ const base = resolve(directory);
10
+ const files = [];
11
+ let total = 0;
12
+ async function walk(folder) {
13
+ for (const entry of await readdir(folder, { withFileTypes: true })) {
14
+ if (
15
+ /^(node_modules|\.git|\.agentdock|\.npmrc|\.ssh|\.aws|\.env(?:\..*)?|\.DS_Store|coverage|test-results)$/i.test(
16
+ entry.name,
17
+ ) ||
18
+ /\.(pem|key|p12|pfx)$/i.test(entry.name)
19
+ )
20
+ continue;
21
+ const full = join(folder, entry.name);
22
+ if ((await lstat(full)).isSymbolicLink())
23
+ throw new Error(`不支持符号链接:${relative(base, full)}`);
24
+ if (entry.isDirectory()) await walk(full);
25
+ else if (entry.isFile()) {
26
+ const bytes = await readFile(full);
27
+ total += bytes.length;
28
+ if (total > 20 * 1024 * 1024 || files.length >= 2000)
29
+ throw new Error('部署最大为 20 MiB / 2000 个文件,请选择构建产物目录');
30
+ files.push({
31
+ path: relative(base, full).split(sep).join('/'),
32
+ content: bytes.toString('base64'),
33
+ });
34
+ }
35
+ }
36
+ }
37
+ await walk(base);
38
+ return files.sort((a, b) => a.path.localeCompare(b.path, 'en'));
39
+ }
40
+
41
+ export async function main(args = process.argv.slice(2)) {
42
+ const command = args.shift() ?? 'help';
43
+ const flags = {};
44
+ const positional = [];
45
+ while (args.length) {
46
+ const arg = args.shift();
47
+ if (arg.startsWith('--')) {
48
+ const name = arg.slice(2);
49
+ if (['no-wait', 'spa', 'build', 'resources', 'no-resources'].includes(name)) flags[name] = true;
50
+ else {
51
+ if (!args[0] || args[0].startsWith('--')) throw new Error(`${arg} 缺少值`);
52
+ flags[name] = args.shift();
53
+ }
54
+ } else positional.push(arg);
55
+ }
56
+ if (command === 'help')
57
+ return {
58
+ usage: [
59
+ 'agentdock init [directory] --project my-site --kind static|node|worker',
60
+ 'agentdock projects',
61
+ 'agentdock deploy [directory] --project my-site --kind static|node|worker [--spa] [--build] [--no-wait]',
62
+ 'agentdock status <deployment-id>',
63
+ 'agentdock logs <deployment-id>',
64
+ 'agentdock promote <project-id> <deployment-id>',
65
+ 'agentdock env <project-id> --file variables.json',
66
+ ],
67
+ authentication:
68
+ 'AGENTDOCK_URL + AGENTDOCK_TOKEN(本地开发自动读取平台 .agentdock/admin-token)',
69
+ output: 'stdout 始终为 JSON;失败退出码 1。默认等待部署完成,--no-wait 立即返回部署 ID。',
70
+ };
71
+ if (command === 'init') {
72
+ const target = resolve(positional[0] ?? '.');
73
+ const kind = flags.kind ?? 'static';
74
+ if (!['static', 'node', 'worker'].includes(kind))
75
+ throw new Error('kind 必须为 static、node 或 worker');
76
+ const config = {
77
+ project: flags.project ?? 'my-site',
78
+ kind,
79
+ directory: '.',
80
+ ...(kind === 'worker' ? { entry: 'worker.js', compatibilityDate: '2026-09-01' } : {}),
81
+ ...(kind === 'node' ? { start: ['node', 'server.mjs'], healthPath: '/' } : { spa: false }),
82
+ };
83
+ await writeFile(join(target, 'agentdock.json'), JSON.stringify(config, null, 2) + '\n', {
84
+ flag: 'wx',
85
+ });
86
+ return { path: join(target, 'agentdock.json'), config };
87
+ }
88
+ const endpoint = (flags.url ?? process.env.AGENTDOCK_URL ?? 'http://127.0.0.1:4400').replace(
89
+ /\/$/,
90
+ '',
91
+ );
92
+ let token = process.env.AGENTDOCK_TOKEN;
93
+ if (!token && /^http:\/\/(127\.0\.0\.1|localhost):4400$/.test(endpoint)) {
94
+ try {
95
+ token = (
96
+ await readFile(new URL('../.agentdock/admin-token', import.meta.url), 'utf8')
97
+ ).trim();
98
+ } catch {}
99
+ }
100
+ if (!token) throw new Error('请设置 AGENTDOCK_TOKEN');
101
+ async function api(path, method = 'GET', body, key) {
102
+ const response = await fetch(endpoint + '/api' + path, {
103
+ method,
104
+ headers: {
105
+ Authorization: `Bearer ${token}`,
106
+ 'Content-Type': 'application/json',
107
+ ...(key ? { 'Idempotency-Key': key } : {}),
108
+ },
109
+ body: body === undefined ? undefined : JSON.stringify(body),
110
+ signal: AbortSignal.timeout(30000),
111
+ });
112
+ const data = await response.json();
113
+ if (!response.ok) {
114
+ const e = new Error(data.error?.message ?? response.statusText);
115
+ e.code = data.error?.code;
116
+ throw e;
117
+ }
118
+ return data;
119
+ }
120
+ if (command === 'projects') return api('/projects');
121
+ if (['resources', 'create-kv', 'create-bucket', 'create-database'].includes(command)) {
122
+ if (!positional[0]) throw new Error('需要项目 ID');
123
+ const base = '/projects/' + encodeURIComponent(positional[0]);
124
+ if (command === 'resources') return api(base + '/resources');
125
+ if (command === 'create-database') return api(base + '/database', 'POST', {});
126
+ if (!flags.name) throw new Error('需要 --name');
127
+ return api(base + (command === 'create-kv' ? '/kv' : '/buckets'), 'POST', { name: flags.name });
128
+ }
129
+ if (command === 'cancel') {
130
+ if (!positional[0]) throw new Error('需要部署 ID');
131
+ return api(`/deployments/${encodeURIComponent(positional[0])}/cancel`, 'POST', {});
132
+ }
133
+ if (command === 'prune') {
134
+ if (!positional[0]) throw new Error('需要项目 ID');
135
+ return api(`/projects/${encodeURIComponent(positional[0])}/prune`, 'POST', {
136
+ keep: Number(flags.keep ?? 5),
137
+ });
138
+ }
139
+ if (command === 'status' || command === 'logs') {
140
+ if (!positional[0]) throw new Error('需要部署 ID');
141
+ return api(
142
+ `/deployments/${encodeURIComponent(positional[0])}${command === 'logs' ? '/logs' : ''}`,
143
+ );
144
+ }
145
+ if (command === 'promote') {
146
+ if (positional.length !== 2) throw new Error('需要项目 ID 和部署 ID');
147
+ return api(`/projects/${encodeURIComponent(positional[0])}/promote`, 'POST', {
148
+ deploymentId: positional[1],
149
+ });
150
+ }
151
+ if (command === 'env') {
152
+ if (!positional[0] || !flags.file) throw new Error('需要项目 ID 和 --file variables.json');
153
+ return api(`/projects/${encodeURIComponent(positional[0])}/env`, 'PUT', {
154
+ values: JSON.parse(await readFile(resolve(flags.file), 'utf8')),
155
+ });
156
+ }
157
+ if (command !== 'deploy') throw new Error(`未知命令:${command}`);
158
+ const base = resolve(positional[0] ?? '.');
159
+ let config = {};
160
+ try {
161
+ config = JSON.parse(await readFile(join(base, 'agentdock.json'), 'utf8'));
162
+ } catch (error) {
163
+ if (error.code !== 'ENOENT') throw error;
164
+ }
165
+ const slug = flags.project ?? config.project;
166
+ if (!slug) throw new Error('请指定 --project 或在 agentdock.json 中设置 project');
167
+ const kind = flags.kind ?? config.kind ?? 'static';
168
+ const files = await collectFiles(resolve(base, config.directory ?? '.'));
169
+ let project = (await api('/projects')).data.find((p) => p.slug === slug);
170
+ if (!project) {
171
+ try {
172
+ project = await api('/projects', 'POST', { name: slug, slug });
173
+ } catch (error) {
174
+ if (error.code !== 'SLUG_TAKEN') throw error;
175
+ project = (await api('/projects')).data.find((p) => p.slug === slug);
176
+ }
177
+ }
178
+ const body = {
179
+ kind,
180
+ files,
181
+ resources:
182
+ flags['no-resources'] === true
183
+ ? false
184
+ : flags.resources === true
185
+ ? true
186
+ : (config.resources ?? kind === 'node'),
187
+ resourceScopes: flags['resource-scopes']
188
+ ? flags['resource-scopes'].split(',').filter(Boolean)
189
+ : config.resourceScopes,
190
+ spa: flags.spa || config.spa || false,
191
+ build: flags.build || config.build || false,
192
+ start: config.start ?? ['node', 'server.mjs'],
193
+ healthPath: config.healthPath ?? '/',
194
+ entry: flags.entry ?? config.entry,
195
+ assets: flags.assets ?? config.assets,
196
+ compatibilityDate: config.compatibilityDate,
197
+ };
198
+ let result = await api(
199
+ `/projects/${project.id}/deployments`,
200
+ 'POST',
201
+ body,
202
+ flags.key ?? randomUUID(),
203
+ );
204
+ if (flags['no-wait']) return result;
205
+ const deadline = Date.now() + 300000;
206
+ while (!['ready', 'failed', 'cancelled', 'deleted'].includes(result.status)) {
207
+ if (Date.now() > deadline) {
208
+ const error = new Error(`等待超时,部署仍可能继续。请执行 agentdock status ${result.id}`);
209
+ error.code = 'WAIT_TIMEOUT';
210
+ throw error;
211
+ }
212
+ await new Promise((r) => setTimeout(r, 750));
213
+ result = await api(`/deployments/${result.id}`);
214
+ }
215
+ if (result.status !== 'ready') {
216
+ const error = new Error(result.error ?? `部署状态:${result.status}`);
217
+ error.code = 'DEPLOYMENT_FAILED';
218
+ error.deploymentId = result.id;
219
+ throw error;
220
+ }
221
+ return result;
222
+ }
223
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
224
+ main()
225
+ .then((result) => console.log(JSON.stringify(result, null, 2)))
226
+ .catch((error) => {
227
+ console.error(
228
+ JSON.stringify(
229
+ {
230
+ error: {
231
+ code: error.code ?? 'CLI_ERROR',
232
+ message: error.message,
233
+ deploymentId: error.deploymentId,
234
+ },
235
+ },
236
+ null,
237
+ 2,
238
+ ),
239
+ );
240
+ process.exitCode = 1;
241
+ });
242
+ }
package/mcp/api.mjs ADDED
@@ -0,0 +1,26 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ export async function platformApi(path, method = 'GET', body) {
3
+ const endpoint = (process.env.AGENTDOCK_URL ?? 'http://127.0.0.1:4400').replace(/\/$/, '');
4
+ let token = process.env.AGENTDOCK_TOKEN;
5
+ if (!token && /^http:\/\/(127\.0\.0\.1|localhost):4400$/.test(endpoint)) {
6
+ try {
7
+ token = (
8
+ await readFile(new URL('../.agentdock/admin-token', import.meta.url), 'utf8')
9
+ ).trim();
10
+ } catch {}
11
+ }
12
+ if (!token) throw new Error('请设置 AGENTDOCK_TOKEN');
13
+ const r = await fetch(endpoint + '/api' + path, {
14
+ method,
15
+ headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
16
+ body: body === undefined ? undefined : JSON.stringify(body),
17
+ signal: AbortSignal.timeout(30000),
18
+ });
19
+ const data = await r.json();
20
+ if (!r.ok) {
21
+ const e = new Error(data.error?.message ?? '请求失败');
22
+ e.code = data.error?.code;
23
+ throw e;
24
+ }
25
+ return data;
26
+ }
package/mcp/index.mjs ADDED
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ import { main } from '../cli/index.mjs';
6
+ import { registerResourceTools } from './resource-tools.mjs';
7
+ import { registerProjectTools } from './project-tools.mjs';
8
+ import { platformApi } from './api.mjs';
9
+ import { saveLocalBinding } from './local-development.mjs';
10
+ const server = new McpServer({ name: 'agentdock', version: '0.2.0' });
11
+ const tool = (name, description, inputSchema, fn, readOnlyHint = false) =>
12
+ server.registerTool(
13
+ name,
14
+ {
15
+ description,
16
+ inputSchema,
17
+ annotations: { readOnlyHint, destructiveHint: !readOnlyHint, idempotentHint: readOnlyHint },
18
+ },
19
+ async (args) => {
20
+ try {
21
+ return { content: [{ type: 'text', text: JSON.stringify(await fn(args)) }] };
22
+ } catch (e) {
23
+ return {
24
+ isError: true,
25
+ content: [
26
+ {
27
+ type: 'text',
28
+ text: JSON.stringify({ error: { code: e.code ?? 'MCP_ERROR', message: e.message } }),
29
+ },
30
+ ],
31
+ };
32
+ }
33
+ },
34
+ );
35
+ tool('list_projects', '列出当前令牌可访问的项目', {}, () => main(['projects']), true);
36
+ registerProjectTools(tool, platformApi);
37
+ tool(
38
+ 'list_resources',
39
+ '列出项目的 KV、数据库和 OSS 资源',
40
+ { projectId: z.string() },
41
+ (a) => main(['resources', a.projectId]),
42
+ true,
43
+ );
44
+ tool(
45
+ 'deploy_site',
46
+ '从本地目录上传静态、Node.js 或 Worker 应用,异步返回部署 ID。需轮询状态确认 ready。Node.js App 默认启用项目资源绑定。开发服务端代码时安装 @co-villa/agentnest-sdk 并使用 createClient(),平台自动注入访问配置;不要让用户手动生成资源令牌,不要将部署令牌作为应用资源令牌。资源需先通过 MCP 创建,数据库轮询到 ready;resources:false 可显式关闭绑定。本地开发先调用 prepare_local_development 并自动保存绑定,再使用 createClient();不要让用户配置 AGENTDOCK_URL/AGENTDOCK_TOKEN 或生成资源令牌。',
47
+ {
48
+ directory: z.string(),
49
+ project: z.string(),
50
+ kind: z.enum(['static', 'node', 'worker']),
51
+ entry: z.string().optional(),
52
+ assets: z.string().optional(),
53
+ spa: z.boolean().optional(),
54
+ build: z.boolean().optional(),
55
+ resources: z.boolean().optional(),
56
+ resourceScopes: z
57
+ .array(z.enum(['kv:read', 'kv:write', 'oss:read', 'oss:write', 'db:query']))
58
+ .optional(),
59
+ idempotencyKey: z.string(),
60
+ },
61
+ (a) =>
62
+ main([
63
+ 'deploy',
64
+ a.directory,
65
+ '--project',
66
+ a.project,
67
+ '--kind',
68
+ a.kind,
69
+ '--key',
70
+ a.idempotencyKey,
71
+ '--no-wait',
72
+ ...(a.entry ? ['--entry', a.entry] : []),
73
+ ...(a.assets ? ['--assets', a.assets] : []),
74
+ ...(a.spa ? ['--spa'] : []),
75
+ ...(a.build ? ['--build'] : []),
76
+ ...(a.resources === false ? ['--no-resources'] : a.resources ? ['--resources'] : []),
77
+ ...(a.resourceScopes ? ['--resource-scopes', a.resourceScopes.join(',')] : []),
78
+ ]),
79
+ );
80
+ tool(
81
+ 'get_deployment',
82
+ '读取部署状态与访问 URL',
83
+ { deploymentId: z.string() },
84
+ (a) => main(['status', a.deploymentId]),
85
+ true,
86
+ );
87
+ tool(
88
+ 'get_logs',
89
+ '读取构建及运行日志',
90
+ { deploymentId: z.string() },
91
+ (a) => main(['logs', a.deploymentId]),
92
+ true,
93
+ );
94
+ tool(
95
+ 'promote_deployment',
96
+ '将项目线上版本切换为指定成功版本',
97
+ { projectId: z.string(), deploymentId: z.string() },
98
+ (a) => main(['promote', a.projectId, a.deploymentId]),
99
+ );
100
+ tool('cancel_deployment', '取消尚未完成的部署', { deploymentId: z.string() }, (a) =>
101
+ main(['cancel', a.deploymentId]),
102
+ );
103
+ registerResourceTools(tool, platformApi, { saveLocalBinding });
104
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,43 @@
1
+ import { mkdir, readFile, writeFile, lstat } from 'node:fs/promises';
2
+ import { resolve, join } from 'node:path';
3
+ export async function saveLocalBinding(binding, directory) {
4
+ const root = resolve(directory),
5
+ folder = join(root, '.agentdock');
6
+ try {
7
+ if ((await lstat(folder)).isSymbolicLink())
8
+ throw new Error('Development binding directory must not be a symlink');
9
+ } catch (e) {
10
+ if (e.code !== 'ENOENT') throw e;
11
+ }
12
+ await mkdir(folder, { recursive: true, mode: 0o700 });
13
+ const target = join(folder, 'development.json');
14
+ try {
15
+ if ((await lstat(target)).isSymbolicLink())
16
+ throw new Error('Development binding file must not be a symlink');
17
+ } catch (e) {
18
+ if (e.code !== 'ENOENT') throw e;
19
+ }
20
+ await writeFile(target, JSON.stringify(binding.config, null, 2) + '\n', { mode: 0o600 });
21
+ const ignore = join(root, '.gitignore');
22
+ let text = '';
23
+ try {
24
+ text = await readFile(ignore, 'utf8');
25
+ } catch (e) {
26
+ if (e.code !== 'ENOENT') throw e;
27
+ }
28
+ if (
29
+ !text
30
+ .split(/\r?\n/)
31
+ .some((line) => ['.agentdock/', '/.agentdock/', '.agentdock'].includes(line.trim()))
32
+ )
33
+ await writeFile(ignore, text + (text && !text.endsWith('\n') ? '\n' : '') + '.agentdock/\n');
34
+ return {
35
+ projectId: binding.config.projectId,
36
+ project: binding.config.project,
37
+ file: target,
38
+ expiresAt: binding.config.expiresAt,
39
+ ready: true,
40
+ instructions:
41
+ '本地绑定已自动保存,直接运行使用 createClient() 的 App。无需设置 AGENTDOCK_URL 或 AGENTDOCK_TOKEN。',
42
+ };
43
+ }
@@ -0,0 +1,54 @@
1
+ import { z } from 'zod';
2
+
3
+ export function registerProjectTools(tool, api) {
4
+ const id = z.string().min(1);
5
+ tool(
6
+ 'create_project',
7
+ '创建项目,需要账号范围的 manage 授权;项目数量仍受账号配额限制',
8
+ {
9
+ name: id.max(80),
10
+ slug: id,
11
+ organizationId: id.optional(),
12
+ },
13
+ (a) => api('/projects', 'POST', a),
14
+ );
15
+ tool(
16
+ 'get_project',
17
+ '查看项目配置、环境变量名称和运行状态',
18
+ { projectId: id },
19
+ (a) => api(`/projects/${encodeURIComponent(a.projectId)}`),
20
+ true,
21
+ );
22
+ tool(
23
+ 'update_project',
24
+ '修改项目名称或关联组织,需要 manage 授权和项目所有者权限;organizationId 为 null 时解除关联',
25
+ {
26
+ projectId: id,
27
+ name: id.max(80).optional(),
28
+ organizationId: id.nullable().optional(),
29
+ },
30
+ ({ projectId, ...body }) => api(`/projects/${encodeURIComponent(projectId)}`, 'PATCH', body),
31
+ );
32
+ tool(
33
+ 'set_project_environment',
34
+ '设置项目环境变量(完整替换),用于后续部署;需要 manage 授权和项目所有者权限。先通过 get_project 检查已有变量名称',
35
+ {
36
+ projectId: id,
37
+ values: z.record(z.string(), z.string()),
38
+ },
39
+ ({ projectId, values }) =>
40
+ api(`/projects/${encodeURIComponent(projectId)}/env`, 'PUT', { values }),
41
+ );
42
+ tool(
43
+ 'set_project_scaling',
44
+ '配置 Kubernetes 应用副本与 CPU 扩缩容目标,需要 manage 授权和项目所有者权限',
45
+ {
46
+ projectId: id,
47
+ min: z.number().int().min(1).max(20),
48
+ max: z.number().int().min(1).max(20),
49
+ cpu: z.number().int().min(10).max(90),
50
+ },
51
+ ({ projectId, ...body }) =>
52
+ api(`/projects/${encodeURIComponent(projectId)}/scale`, 'POST', body),
53
+ );
54
+ }
@@ -0,0 +1,194 @@
1
+ import { z } from 'zod';
2
+ // Shared registrations for remote HTTP and packaged stdio MCP; API remains the authority.
3
+ export function registerResourceTools(tool, api, { saveLocalBinding } = {}) {
4
+ const id = z.string().min(1),
5
+ project = { projectId: id },
6
+ enc = encodeURIComponent;
7
+ const base = (a) => `/projects/${enc(a.projectId)}`;
8
+ tool(
9
+ 'prepare_local_development',
10
+ 'Agent 在本地启动 App 前默认自动调用本工具,复用当前 MCP 项目授权准备开发。Agent 必须自动将返回的 config 写入 App 根目录 .agentdock/development.json,并确保 .agentdock/ 被 git 忽略。不要让用户复制令牌或手动设置 AGENTDOCK_URL/AGENTDOCK_TOKEN。本地 SDK 使用 createClient() 自动读取并续期。绑定24小时有效,不包含 MCP 原始 OAuth 凭据。',
11
+ { ...project, ...(saveLocalBinding ? { directory: z.string().optional() } : {}) },
12
+ async (a) => {
13
+ const binding = await api(base(a) + '/local-development', 'POST', {});
14
+ return saveLocalBinding ? saveLocalBinding(binding, a.directory ?? '.') : binding;
15
+ },
16
+ );
17
+ const kv = (a) => `${base(a)}/kv/${enc(a.namespace)}`;
18
+ const bucket = (a) => `${base(a)}/buckets/${enc(a.bucketId)}`;
19
+ tool(
20
+ 'get_database',
21
+ '查看项目数据库状态、模式与连接地址,不返回密码',
22
+ project,
23
+ (a) => api(base(a) + '/database'),
24
+ true,
25
+ );
26
+ tool(
27
+ 'create_database',
28
+ '创建共享服务中的独立项目数据库和账号。异步返回;调用 get_database 等待 ready。部署权限可用,专属付费实例尚未开放。',
29
+ project,
30
+ (a) => api(base(a) + '/database', 'POST', { mode: 'shared' }),
31
+ );
32
+ tool(
33
+ 'query_database',
34
+ '在项目数据库执行参数化 SQL。最多 1000 行 / 1 MiB,超时 5 秒;可能修改数据,需要部署权限。',
35
+ {
36
+ ...project,
37
+ sql: z.string().min(1).max(32768),
38
+ params: z.array(z.unknown()).max(100).optional(),
39
+ },
40
+ (a) => api(base(a) + '/database/query', 'POST', { sql: a.sql, params: a.params ?? [] }),
41
+ );
42
+ tool(
43
+ 'get_database_credentials',
44
+ '返回项目数据库连接密码,仅项目所有者完整身份可用,部署 Token 不可用。不要将凭证写入公开代码或日志。',
45
+ project,
46
+ (a) => api(base(a) + '/database/credentials', 'POST', {}),
47
+ true,
48
+ );
49
+ tool(
50
+ 'delete_database',
51
+ '永久删除项目数据库,需要所有者身份;confirm 必须等于项目 slug。',
52
+ { ...project, confirm: id },
53
+ (a) => api(base(a) + '/database', 'DELETE', { confirm: a.confirm }),
54
+ );
55
+ tool(
56
+ 'list_kv_namespaces',
57
+ '列出当前项目 KV 命名空间',
58
+ project,
59
+ (a) => api(base(a) + '/kv'),
60
+ true,
61
+ );
62
+ tool(
63
+ 'create_kv_namespace',
64
+ '创建项目 KV 命名空间,需要部署权限',
65
+ { ...project, name: z.string().regex(/^[a-z][a-z0-9_-]{0,39}$/) },
66
+ (a) => api(base(a) + '/kv', 'POST', { name: a.name }),
67
+ );
68
+ tool(
69
+ 'delete_kv_namespace',
70
+ '永久删除命名空间及所有键,需要项目所有者身份和名称确认',
71
+ { ...project, namespace: id, confirm: id },
72
+ (a) => api(kv(a), 'DELETE', { confirm: a.confirm }),
73
+ );
74
+ tool(
75
+ 'list_kv_entries',
76
+ '分页列出 KV 键;返回 cursor,下一页作为 after 传入',
77
+ { ...project, namespace: id, prefix: z.string().optional(), after: z.string().optional() },
78
+ (a) => {
79
+ const q = new URLSearchParams();
80
+ if (a.prefix !== undefined) q.set('prefix', a.prefix);
81
+ if (a.after !== undefined) q.set('after', a.after);
82
+ return api(kv(a) + '/entries?' + q);
83
+ },
84
+ true,
85
+ );
86
+ tool(
87
+ 'get_kv_entry',
88
+ '读取项目 KV JSON 值和过期时间',
89
+ { ...project, namespace: id, key: id },
90
+ (a) => api(kv(a) + '/entries/' + enc(a.key)),
91
+ true,
92
+ );
93
+ tool(
94
+ 'put_kv_entry',
95
+ '写入 KV JSON 值,ttl 为秒,省略表示永久;需要部署权限',
96
+ {
97
+ ...project,
98
+ namespace: id,
99
+ key: id,
100
+ value: z.unknown(),
101
+ ttl: z.number().int().min(1).max(31536000).optional(),
102
+ },
103
+ (a) =>
104
+ api(kv(a) + '/entries/' + enc(a.key), 'PUT', {
105
+ value: a.value,
106
+ ...(a.ttl === undefined ? {} : { ttl: a.ttl }),
107
+ }),
108
+ );
109
+ tool(
110
+ 'delete_kv_entry',
111
+ '删除项目 KV 键,需要部署权限',
112
+ { ...project, namespace: id, key: id },
113
+ (a) => api(kv(a) + '/entries/' + enc(a.key), 'DELETE'),
114
+ );
115
+ tool(
116
+ 'list_storage_buckets',
117
+ '列出项目 OSS 存储桶及用量',
118
+ project,
119
+ (a) => api(base(a) + '/buckets'),
120
+ true,
121
+ );
122
+ tool(
123
+ 'create_storage_bucket',
124
+ '创建私有 OSS 存储桶,需要部署权限',
125
+ { ...project, name: z.string().regex(/^[a-z][a-z0-9-]{2,39}$/) },
126
+ (a) => api(base(a) + '/buckets', 'POST', { name: a.name }),
127
+ );
128
+ tool(
129
+ 'delete_storage_bucket',
130
+ '删除空存储桶,需要项目所有者身份;非空桶拒绝删除',
131
+ { ...project, bucketId: id },
132
+ (a) => api(bucket(a), 'DELETE'),
133
+ );
134
+ tool(
135
+ 'list_storage_objects',
136
+ '列出项目 OSS 存储桶对象',
137
+ { ...project, bucketId: id },
138
+ (a) => api(bucket(a) + '/objects'),
139
+ true,
140
+ );
141
+ tool(
142
+ 'put_storage_object',
143
+ '上传 OSS 文件,content 为 Base64,文件最多 10 MiB,需要部署权限',
144
+ { ...project, bucketId: id, name: id, content: z.string().max(14 * 1024 * 1024) },
145
+ (a) => api(bucket(a) + '/objects', 'POST', { name: a.name, content: a.content }),
146
+ );
147
+ tool(
148
+ 'sign_storage_object',
149
+ '生成对象限时下载链接,链接包含临时授权,请勿公开',
150
+ {
151
+ ...project,
152
+ bucketId: id,
153
+ objectId: id,
154
+ seconds: z.number().int().min(1).max(3600).optional(),
155
+ },
156
+ (a) =>
157
+ api(bucket(a) + '/sign', 'POST', {
158
+ objectId: a.objectId,
159
+ ...(a.seconds === undefined ? {} : { seconds: a.seconds }),
160
+ }),
161
+ true,
162
+ );
163
+ tool(
164
+ 'delete_storage_object',
165
+ '删除 OSS 对象,需要部署权限',
166
+ { ...project, bucketId: id, objectId: id },
167
+ (a) => api(bucket(a) + '/objects/' + enc(a.objectId), 'DELETE'),
168
+ );
169
+ tool(
170
+ 'list_domains',
171
+ '列出 App 自定义域名、TXT 验证记录及验证状态',
172
+ project,
173
+ (a) => api(base(a) + '/domains'),
174
+ true,
175
+ );
176
+ tool(
177
+ 'add_domain',
178
+ '绑定 App 域名并生成 TXT 验证记录,需要项目所有者身份',
179
+ { ...project, hostname: id },
180
+ (a) => api(base(a) + '/domains', 'POST', { hostname: a.hostname }),
181
+ );
182
+ tool(
183
+ 'verify_domain',
184
+ '验证 App 域名的 DNS TXT 所有权,需要项目所有者身份;仅验证成功后启用路由',
185
+ { ...project, hostname: id },
186
+ (a) => api(base(a) + '/domains/verify', 'POST', { hostname: a.hostname }),
187
+ );
188
+ tool(
189
+ 'remove_domain',
190
+ '解绑 App 自定义域名,需要项目所有者身份',
191
+ { ...project, hostname: id },
192
+ (a) => api(base(a) + '/domains/' + enc(a.hostname), 'DELETE'),
193
+ );
194
+ }
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "@co-villa/agentnest-tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "AgentNest deployment CLI and MCP stdio server",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
9
+ "files": [
10
+ "cli/index.mjs",
11
+ "mcp/index.mjs",
12
+ "mcp/resource-tools.mjs",
13
+ "mcp/project-tools.mjs",
14
+ "mcp/api.mjs",
15
+ "mcp/local-development.mjs"
16
+ ],
17
+ "bin": {
18
+ "agentnest": "./cli/index.mjs",
19
+ "agentnest-mcp": "./mcp/index.mjs"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.30.0",
23
+ "zod": "^4.6.5"
24
+ }
6
25
  }