Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/dist
**/node_modules
**/.env
**/.cloudbase-sites/
**/*.dxt
# Environment variables
.env.local
Expand Down
2 changes: 1 addition & 1 deletion doc/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ CloudBase 云函数统一写入口。支持创建函数、更新代码、更新
---

### `manageHosting`
管理 CloudBase 静态托管的变更操作。action=upload 上传本地构建产物;action=delete 删除托管文件或目录(必须 confirm=true);action=setWebsiteDocument 设置首页/错误页/路由规则;action=enableService 开通静态托管;action=bindDomain / unbindDomain / updateDomain 管理自定义域名;action=downloadFile / downloadDirectory 下载托管内容到本地。若任务只是查看配置、文件或域名状态,请改用 queryHosting。
管理 CloudBase 静态托管的变更操作。action=upload 上传本地构建产物到共享域名(域名格式:<envId>-<appId>.tcloudbaseapp.com/<cloudPath>);action=delete 删除托管文件或目录(必须 confirm=true);action=setWebsiteDocument 设置首页/错误页/路由规则;action=enableService 开通静态托管;action=bindDomain / unbindDomain / updateDomain 管理自定义域名;action=downloadFile / downloadDirectory 下载托管内容到本地。⚠️ 新项目部署优先使用 manageApps(部署到独立子域名),本工具适合已有老项目继续使用或作为 manageApps 的 fallback。manageApps 与 manageHosting 域名不同,切换会导致老链接失效。若任务只是查看配置、文件或域名状态,请改用 queryHosting。

#### 参数

Expand Down
29 changes: 29 additions & 0 deletions mcp/src/tools/apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ describe("app tools", () => {
mockDescribeAppInfo.mockResolvedValue({
ServiceName: "demo-app",
DeployType: "static-hosting",
Domain: "demo-app-env-test.webapps.tcloudbase.com",
RequestId: "req-app-info",
});
mockDescribeAppVersionList.mockResolvedValue({
Expand Down Expand Up @@ -153,15 +154,43 @@ describe("app tools", () => {
buildType: "ZIP",
}),
);
expect(mockDescribeAppInfo).toHaveBeenCalledWith({
deployType: "static-hosting",
serviceName: "demo-app",
});
expect(payload).toMatchObject({
success: true,
data: {
action: "deployApp",
serviceName: "demo-app",
domain: "demo-app-env-test.webapps.tcloudbase.com",
accessUrl: "https://demo-app-env-test.webapps.tcloudbase.com",
},
});
});

it("manageApps(action=deployApp) should normalize access URL from app details", async () => {
mockDescribeAppInfo.mockResolvedValueOnce({
ServiceName: "demo-app",
DeployType: "static-hosting",
Domain: "https://demo-app-env-test.webapps.tcloudbase.com/",
RequestId: "req-app-info",
});

const result = await tools.manageApps.handler({
action: "deployApp",
serviceName: "demo-app",
filePath: "/tmp/demo-app",
buildPath: "dist",
});
const payload = JSON.parse(result.content[0].text);

expect(payload.data.domain).toBe("demo-app-env-test.webapps.tcloudbase.com");
expect(payload.data.accessUrl).toBe("https://demo-app-env-test.webapps.tcloudbase.com");
expect(payload.data.accessUrlSource).toBe("describeAppInfo.Domain");
expect(payload.data.nextStep.hint).toContain("accessUrl");
});

it("manageApps(action=deployApp) with cosTimestamp should skip uploadCode", async () => {
const result = await tools.manageApps.handler({
action: "deployApp",
Expand Down
48 changes: 45 additions & 3 deletions mcp/src/tools/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ function getCloudAppService(cloudbase: any) {
return cloudbase.cloudAppService ?? cloudbase.getCloudAppService?.();
}

function normalizeAccessUrlFromDomain(domain: unknown): { domain?: string; accessUrl?: string } {
if (typeof domain !== "string" || !domain.trim()) return {};
const trimmed = domain.trim();
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
try {
const url = new URL(withProtocol);
url.hash = "";
url.search = "";
url.pathname = url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "");
return {
domain: url.host,
accessUrl: url.toString().replace(/\/$/, ""),
};
} catch {
return {};
}
}

export function registerAppTools(server: ExtendedMcpServer) {
const cloudBaseOptions = server.cloudBaseOptions;
const getManager = () => getCloudBaseManager({ cloudBaseOptions });
Expand Down Expand Up @@ -288,7 +306,7 @@ export function registerAppTools(server: ExtendedMcpServer) {
appPath: z
.string()
.optional()
.describe("应用线上访问路径(hosting mount path),例如 /my-web-app。不是本地目录路径;省略时默认为 /serviceName。"),
.describe("应用线上访问路径(hosting mount path),例如 /my-web-app。不是本地目录路径;CloudApp 已有独立子域名,省略时默认为 /(根路径)。"),
buildPath: z
.string()
.optional()
Expand Down Expand Up @@ -476,13 +494,33 @@ export function registerAppTools(server: ExtendedMcpServer) {
logCloudBaseResult(server.logger, result);

const { BuildId, VersionName } = result;
let appInfo: Record<string, unknown> | undefined;
let domain: string | undefined;
let accessUrl: string | undefined;
let accessUrlLookupWarning: string | undefined;
try {
appInfo = await appService.describeAppInfo({
deployType: "static-hosting",
serviceName,
});
logCloudBaseResult(server.logger, appInfo);
({ domain, accessUrl } = normalizeAccessUrlFromDomain(appInfo?.Domain));
} catch (error) {
accessUrlLookupWarning = error instanceof Error ? error.message : String(error);
}

return jsonContent(
buildEnvelope(
{
action,
serviceName,
versionName: VersionName,
buildId: BuildId,
domain,
accessUrl,
accessUrlSource: accessUrl ? "describeAppInfo.Domain" : undefined,
accessUrlLookupWarning,
app: appInfo,
upload: { cosTimestamp: cosTs },
deployment: result,
buildConfig: {
Expand All @@ -498,10 +536,14 @@ export function registerAppTools(server: ExtendedMcpServer) {
serviceName,
buildId: BuildId,
},
hint: `调用 queryApps(action="getAppVersion", serviceName="${serviceName}", buildId="${BuildId}") 轮询构建状态,直到 status 变为 SUCCESS 或 FAILED。构建通常需要 3~5 分钟。若状态为 FAILED,可继续调用 queryApps(action="getBuildLog", serviceName="${serviceName}", buildId="${BuildId}") 查看构建日志诊断失败原因。`,
hint: accessUrl
? `调用 queryApps(action="getAppVersion", serviceName="${serviceName}", buildId="${BuildId}") 轮询构建状态,直到 status 变为 SUCCESS 或 FAILED。构建成功后,后续记录部署时必须使用本结果的 accessUrl=${accessUrl},不要自行拼接域名。若状态为 FAILED,可继续调用 queryApps(action="getBuildLog", serviceName="${serviceName}", buildId="${BuildId}") 查看构建日志诊断失败原因。`
: `调用 queryApps(action="getAppVersion", serviceName="${serviceName}", buildId="${BuildId}") 轮询构建状态,直到 status 变为 SUCCESS 或 FAILED;再调用 queryApps(action="getApp", serviceName="${serviceName}") 读取 app.Domain 作为 accessUrl,不能自行拼接域名。若状态为 FAILED,可继续调用 queryApps(action="getBuildLog", serviceName="${serviceName}", buildId="${BuildId}") 查看构建日志诊断失败原因。`,
},
},
"CloudBase 应用构建已触发,请通过 queryApps 轮询构建状态。",
accessUrl
? "CloudBase 应用构建已触发,已返回真实 accessUrl;请通过 queryApps 轮询构建状态。"
: "CloudBase 应用构建已触发,请通过 queryApps 轮询构建状态,并用 getApp 读取真实域名。",
),
);
}
Expand Down
22 changes: 22 additions & 0 deletions plugin/cloudbase-sites/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "cloudbase-sites",
"description": "CloudBase Sites — create, save, deploy, and inspect React/Vite web apps hosted on Tencent CloudBase. Inspired by Codex Sites' two-stage save→deploy pattern; powered by cloudbase-mcp.",
"version": "0.1.0",
"author": {
"name": "Tencent CloudBase",
"url": "https://cloudbase.net"
},
"homepage": "https://github.com/TencentCloudBase/cloudbase-mcp",
"license": "MIT",
"keywords": [
"cloudbase",
"sites",
"vibe-coding",
"vite",
"react",
"vue",
"claude-code",
"codebuddy",
"openclaw"
]
}
44 changes: 44 additions & 0 deletions plugin/cloudbase-sites/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "cloudbase-sites",
"version": "0.1.0",
"description": "CloudBase Sites lets Codex create, preview, save, deploy, inspect, and roll back Vite web apps hosted on Tencent CloudBase.",
"author": {
"name": "Tencent CloudBase",
"url": "https://cloudbase.net"
},
"homepage": "https://github.com/TencentCloudBase/cloudbase-mcp",
"repository": "https://github.com/TencentCloudBase/cloudbase-mcp",
"license": "MIT",
"keywords": [
"cloudbase",
"sites",
"codex",
"vibe-coding",
"vite",
"react",
"vue",
"deployment"
],
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"interface": {
"displayName": "CloudBase Sites",
"shortDescription": "Build, preview, version, and deploy sites to Tencent CloudBase.",
"longDescription": "Create Vite-based React or Vue web apps, preview them locally, save versions, deploy to CloudBase CloudApp independent domains, and add CloudBase database, storage, auth, functions, or CloudRun backend capabilities through cloudbase-mcp.",
"developerName": "Tencent CloudBase",
"category": "Developer Tools",
"capabilities": [
"Interactive",
"Write"
],
"websiteURL": "https://cloudbase.net",
"privacyPolicyURL": "https://cloud.tencent.com/document/product/301/11470",
"termsOfServiceURL": "https://cloud.tencent.com/document/product/301/1967",
"defaultPrompt": [
"Build a CloudBase site and preview it locally.",
"Save this version, then deploy it to CloudBase.",
"Add database-backed state to this CloudBase site."
],
"brandColor": "#006EFF"
}
}
9 changes: 9 additions & 0 deletions plugin/cloudbase-sites/.mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"mcpServers": {
"cloudbase-mcp": {
"command": "npx",
"args": ["-y", "@cloudbase/cloudbase-mcp@latest"],
"env": {}
}
}
}
26 changes: 26 additions & 0 deletions plugin/cloudbase-sites/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# CloudBase Sites Plugin

CloudBase Sites packages the shared CloudBase site runtime for Claude Code,
CodeBuddy, and Codex.

## Codex local validation

Validate the Codex plugin manifest from the repository root:

```bash
python3 /Users/bookerzhao/.codex/skills/.system/plugin-creator/scripts/validate_plugin.py plugin/cloudbase-sites
```

Run the focused regression tests:

```bash
cd mcp
node_modules/.bin/vitest run --config vitest.config.js ../tests/cloudbase-sites-plugin.test.js
```

## Runtime state

Project-local runtime state is stored in `<project>/.cloudbase-sites/`.
Machine-level supervisor state is stored in `~/.cloudbase-sites/`.

These paths are runtime-only and should not be committed.
Loading
Loading