Wrap a dedicated CloudBase MCP for your teams
In one sentence: hand each team member one MCP config, and they can deploy and manage CloudBase apps from the AI IDE they already use — no Tencent Cloud account, no visibility into other people's data, no way to delete production resources.
Estimated time: 1 hour | Difficulty: advanced
The simpler path: if you just want to get started, read "First: most teams don't need a wrapper" — one prompt, no wrapper. Everything below is the route once you need governance.
When this applies, and when it doesn't
Applies to: a unified entry point for an internal dev platform, an external SaaS that generates apps for customers, per-person or per-project environment isolation, audit trails of tool calls.
Not for: individual developers (use the official MCP directly); "the model usually shouldn't do X" (a System Prompt is enough); customers demanding zero plaintext credentials (use custom authorization codes — this guide uses API Keys); the international site (different endpoints and account system).
First: three layers, take what you need
"Governance" for enterprise integration is not wrapper-or-nothing. There are three layers with escalating constraint strength. Positioning first: Layer 1 changes no code — an expert plus skills is enough. When the soft constraints of experts / skills can't meet governance requirements (can't block accidental deletes or privilege escalation, no credential lifecycle, no audit), you build a dedicated MCP — that's Layer 2 / 3, and it does involve AI-assisted development (what this guide teaches). Layer 2 vs Layer 3 differs only in where it runs: on the employee's machine, or on corporate servers.
Layer 1 · Custom experts / System Prompt / Skill: no code changes. Prompts and skills inject corporate conventions into the AI — cloud functions must carry VPC config, unified resource naming, even a bundled authorization helper for zero-plaintext auth. Constraints live at the prompt layer (soft, can be bypassed), lowest cost. See Enterprise agent platform integration best practices.
Layer 2 · Wrap a local MCP (this guide): hard governance at the protocol layer — tool trimming, dangerous-action interception, per-person credential delivery and audit. Runs on the employee's machine (stdio).
Layer 3 · Wrap a remote MCP: the wrapper runs as a remote service, employees install nothing. Same source code as Layer 2; the deploy flow is already designed for the remote shape (source code goes through the COS channel, no local files needed). Differences at the end, in "What changes in the remote shape".
Getting connected has no layers: the entry barrier for Layer 1 is one prompt. The employee gets only an environment ID and an API Key, and sends this from any AI tool's chat:
Set up CloudBase for me, following these steps:
1. Open https://docs.cloudbase.net/skill.md and complete the setup as instructed.
2. When done, tell me and suggest the most relevant next step.
My environment id is env-xxxxx, apikey is ak-xxxxx
The AI opens the official setup instructions (skill.md) and configures everything itself — the employee never needs to know the MCP config format.
The three layers side by side:
| Layer 1 · Custom expert | Layer 2 · Local MCP (this guide) | Layer 3 · Remote MCP | |
|---|---|---|---|
| Where the constraint lives | Prompt layer (soft, can be bypassed) | Protocol layer (hard, cannot be bypassed) | Protocol layer + unified server-side operation |
| Tool surface | Full official set, including the capi passthrough | Trimmed to corporate policy | Same as left |
| Dangerous actions | Prompts regulate behavior | Intercepted at the protocol layer | Intercepted at the protocol layer |
| Credentials | Helper can be bundled for zero-plaintext auth | Platform delivers per person, revocable, audited | Held server-side; employees hold nothing |
| Where it runs | Expert / skill in the employee's config | Employee's machine (stdio) | Corporate server (HTTP endpoint) |
Don't want to pick a layer yourself? Send the official AI integration prompt to your AI coding assistant (Cursor, Copilot, or your in-house agent platform). It reads the docs, clarifies your architecture and compliance requirements, recommends an option, and helps you land it:
I want to integrate Tencent CloudBase's enterprise-grade agent platform capabilities into the current project.
Please read these references first:
1. CloudBase Skill spec: https://docs.cloudbase.net/skill.md
2. Enterprise platform integration best practices: https://docs.cloudbase.net/en/solutions/cloudbase-platform-edition/enterprise-integration
If the **cloud-api-operations** skill is installed in your environment, load it first and read its `references/recipes/enterprise-mcp-wrapper.md` (the full hands-on recipe for wrapping a dedicated enterprise MCP); if not installed, work from the documents above.
Then ask me about our current system architecture, authentication model, and compliance requirements. Discuss with me and evaluate the best integration option (three layers: custom experts / System Prompt, wrapping a local MCP, wrapping a remote MCP). After we agree on the approach, guide me through implementation step by step. For any cloud API name, parameter, or behavior during implementation, defer to the docs and real tests — never infer from memory.
The decision rule in one line: make it work, then soft constraints, then hard governance — what the prompt layer can't block (accidental deletes, privilege escalation, credential leaks) is what needs a wrapper.
Overall structure
Two surfaces: day-to-day work happens entirely in the chat (deploy, preview, check status, database); release actions only exist in the pipeline config — set up once by a maintainer, triggered by git push, never touched by employees.
The pipeline is CloudBase's built-in release pipeline (push code, it builds and deploys). "Pipeline" in this guide always means it. Teams with their own CI (GitHub Actions, Jenkins, etc.) can plug in too; configuring that is out of scope here.
The same wrapper code has two deployment shapes: local stdio (the diagram above, on the employee's machine) or a remote HTTP service (on corporate servers; the employee config connects to a URL instead of running npx). The Step 4 deploy flow works for both shapes — that's why source code always goes through the COS channel.
Step 1: one environment per person, deliver the config
Use CreateEnv to create one environment per person (or per project), and maintain the userId → envId mapping. Two isolation rules: one environment per person (a leaked credential affects one person) and one environment per app (mixing apps means mixed-up collections, hard overwrites of same-named functions, static hosting clobbering each other, and no per-app metering). Then deliver this config to each person:
{
"mcpServers": {
"cloudbase": {
"command": "npx",
"args": ["-y", "@your-org/cloudbase-wrapper"],
"env": {
"CLOUDBASE_ENV_ID": "env-xxxxx",
"CLOUDBASE_API_KEY": "ak-xxxxx"
}
}
}
}
@your-org/cloudbase-wrapper is a placeholder — it's the package name the wrapper from Step 2 gets published under; replace it with yours. If you've never published to npm, see "Ship the wrapper" at the end of Step 2.
The Key is an environment-scoped credential (a leak affects one person's environment). Create it with CreateApiKey, optionally scoped by whitelist (e.g. frontend team gets "static hosting - write" only). Three rules: one Key per person, delete on departure, rotate on schedule (an environment holds at most 5 Keys — rotate rolling, no downtime).
The only credential shape delivered to the AI is environment ID + API Key. The API Key is the long-lived root credential; internally the MCP exchanges it for 30-minute STS temporary credentials for outbound calls and re-exchanges automatically on expiry. The STS trio is an exchange artifact, not a deliverable — hand out STS alone and when it expires there is no root credential to re-exchange with, and the whole chain breaks.
When the wrapper runs as a remote service, this config becomes a URL connection ("url": "https://mcp.your-org.com/..." plus an auth header), and credentials are held server-side instead of delivered to employee machines — choose the delivery shape per your security baseline; the steps that follow don't change.
Step 2: write the wrapper
One file. The official MCP runs in the same process, wired through an in-memory transport; the outer layer does three things: trim tools, intercept dangerous actions, verify credentials.
import { createRequire } from 'node:module';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
// Must go through the CJS entry: the ESM entry has undeclared internal dependencies
const require = createRequire(import.meta.url);
const { createCloudBaseMcpServer } = require('@cloudbase/cloudbase-mcp');
// The plugin set is platform policy, hard-coded in the wrapper. Employees don't configure it. Adjust as needed:
const PLUGINS = 'env,database,functions,storage,hosting,gateway,cloudrun,apps'.split(',');
// Omitting it turns on everything by default, including `capi`, which passes through arbitrary cloud APIs — enterprise setups must exclude it
const upstream = await createCloudBaseMcpServer({
name: 'cloudbase-mcp', cloudMode: false,
pluginsEnabled: PLUGINS,
cloudBaseOptions: { envId: process.env.CLOUDBASE_ENV_ID },
});
const [ct, st] = InMemoryTransport.createLinkedPair();
await upstream.connect(st);
const client = new Client({ name: 'wrapper', version: '1.0.0' }, { capabilities: {} });
await client.connect(ct);
// Dangerous actions: the delete-family from the official confirmation list, matched by semantic prefix (delete/destroy/remove...)
const isDenied = (tool, action) =>
/^(delete|destroy|remove|clear|truncate|drop)/i.test(action ?? '');
const wrapper = new Server({ name: 'cloudbase-wrapper', version: '1.0.0' }, { capabilities: { tools: {} } });
wrapper.setRequestHandler(ListToolsRequestSchema, async () => {
const { tools } = await client.listTools();
const visible = [];
for (const tool of tools) {
// Denied actions are cut from the parameter enum, so the model can't even see them in the tool definition
const p = tool.inputSchema?.properties?.action;
if (p?.enum) {
p.enum = p.enum.filter((a) => !isDenied(tool.name, a));
if (p.enum.length === 0) continue;
}
visible.push(tool);
}
return { tools: visible };
});
wrapper.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args = {} } = req.params;
if (isDenied(name, args.action)) {
return { content: [{ type: 'text', text: `${name}.${args.action} is blocked by platform policy; retrying will not help.` }], isError: true };
}
console.error('[audit]', JSON.stringify({ ts: Date.now(), env: process.env.CLOUDBASE_ENV_ID, call: `${name}.${args.action ?? ''}` }));
return client.callTool(req.params);
});
// Credential self-check: only single_env + env source passes, so we never silently fall back to an account-level global login
const res = await client.callTool({ name: 'auth', arguments: { action: 'status' } });
const text = (res.content ?? []).map((c) => c.text ?? '').join('\n');
const scope = /credential_scope["'=:\s]+(\w+)/i.exec(text)?.[1];
if (scope && scope !== 'single_env') {
console.error(`[cbw] unexpected credential scope (${scope}); API Key not in effect, refusing to serve`);
process.exit(1);
}
await wrapper.connect(new StdioServerTransport());
console.error('[cbw] ready');
Three things you cannot skip:
- All logs go to
console.error. stdio is line-delimited JSON-RPC — one stray character on stdout crashes the client pluginsEnabledmust be explicit. Omitting it turns on everything, includingcapi— which can call arbitrary cloud APIs, bypassing the entire whitelist- Both filter and intercept. Trim only the list and the model will guess tool names and call them anyway; intercept only calls and the model sees tools it can't use and retries forever
Ship the wrapper
The Step 2 file is a standard npm package: add a package.json (name set to your package name, e.g. @your-org/cloudbase-wrapper), npm publish, and the npx -y <package> in the Step 1 config downloads and runs it automatically — employees pre-install nothing.
No public npm access? Two alternatives: publish to a corporate registry (an artifact repo or verdaccio; employees add one .npmrc line pointing at it), or install straight from an internal git repo (npx git+ssh://..., no publishing). Releases are owned by the platform team; pin the version (e.g. pkg@1.2.0) and roll upgrades from the platform side, so npx never pulls a different version than expected.
Step 3: verify the credential once
The API Key has a subtle behavior: it is only picked up when the Key and CLOUDBASE_ENV_ID are both present in the environment variables, and a failed exchange is silently swallowed — the wrapper falls back to whatever account-level login state exists on the machine and keeps running with account-wide permissions, unnoticed.
That's why the self-check in Step 2 must stay: credential_scope=single_env and credential_source=env is what "the Key works" means. For debugging you can skip the self-check via an environment variable to verify the protocol layer; keep it on in production.
The good news: the API Key is not used per call — the MCP exchanges it for 30-minute temporary credentials before going out; the Key touches the network only during the exchange.
Step 4: deploy
Deployment is one action with two triggers: during development, say "deploy" in the chat and the model goes through MCP tools; for a formal release, the same config goes into git and CloudBase's built-in release pipeline executes it. Both triggers produce continuous versions of the same CloudApp.
The code in this section is pseudocode showing which MCP tools to call with which parameters (via the CloudBase MCP or your enterprise wrapper MCP) — the parameters and flow are the point, not runnable JS.
Source code always goes through the COS channel:
getUploadUrlreturns a presigned URL → PUT the source zip →deployAppwithsource: { type: 'zip', cosTimestamp }triggers the build (theTypeenum only hasgit/zip; in the two-phase uploadrepostays empty and the platform locates the just-uploaded package by timestamp). All three examples follow this pattern. The flow depends on no local files — it works wherever the caller lives (local stdio or remote HTTP), so it still holds when the wrapper runs as a remote MCP. The local shape also has afilePathshortcut (the MCP reads a local directory, packages and uploads); it's not used in the examples.
Pick your scenario:
| Your app | Path | Tools |
|---|---|---|
| Static / standard framework | One upload + one deployApp | manageApps |
| HTTP function (custom image backend) | Upload + build image → attach as a function | manageApps + manageFunctions |
| Full-stack in one app | Upload + one deployApp: serviceList + routes | manageApps |
Example 1: static / standard framework
// 1. Get the presigned upload URL
const { uploadUrl, uploadHeaders, unixTimestamp } = await manageApps({
action: 'getUploadUrl',
serviceName: 'crm-portal',
});
// 2. PUT the source zip to uploadUrl (with uploadHeaders)
// 3. Trigger deploy: the cloud pulls the package and builds
await manageApps({
action: 'deployApp',
serviceName: 'crm-portal', // Domain: crm-portal-<envId>.webapps.tcloudbase.com
source: { type: 'zip', cosTimestamp: unixTimestamp }, // the unixTimestamp returned in step 1
framework: 'react',
installCmd: 'npm ci',
buildCmd: 'npm run build',
buildPath: 'dist', // artifact directory, relative to the project root
});
// 4. Poll status
await queryApps({ action: 'getAppVersion', serviceName: 'crm-portal', buildId });
// 5. On failure, read the logs
await queryApps({ action: 'getBuildLog', serviceName: 'crm-portal', buildId });
Parameter notes: buildPath is the artifact directory relative to the project root; non-standard builds don't need different tools — installCmd / buildCmd accept any command (e.g. pnpm --filter app build).
Example 2: HTTP function (custom image backend)
Build and deploy are two phases: the build container has no deploy command — the build ends with pushing the image to the registry; deployment happens in a second tool call, outside the container.
// 0. Upload source (same as Example 1: getUploadUrl → PUT zip; for custom, getUploadUrl also carries deployType: 'custom')
// Stage A: build the image and push (manageApps custom, inside the container)
await manageApps({
action: 'deployApp',
serviceName: 'order-api',
deployType: 'custom',
source: { type: 'zip', cosTimestamp: unixTimestamp }, // same as Example 1: the unixTimestamp from getUploadUrl
// IMAGE = registry host/namespace/service, set by your org; the tag must be $CLOUDBASE_VERSION_NAME
customSteps: [
{ name: 'build', command: 'docker build -t $IMAGE:$CLOUDBASE_VERSION_NAME .' },
{ name: 'push', command: 'docker push $IMAGE:$CLOUDBASE_VERSION_NAME' },
],
});
// No login credentials needed: the platform injects $API_SECRET_ID / $API_SECRET_KEY / $API_TOKEN;
// sign TCR CreateInstanceToken for a temporary token, then docker login --password-stdin
// Stage B: attach the image as an HTTP function (outside the container)
await manageFunctions({
action: 'createFunction',
func: {
name: 'order-api',
runtime: 'CustomImage',
imageConfig: { imageUri: '$IMAGE:$CLOUDBASE_VERSION_NAME' },
},
});
// Later iterations use updateFunctionCode + imageConfig; no need to recreate the function
Key points: the container listens on port 9000 and starts the HTTP server itself; image ≤ 500MB (cold start); imageUri must carry a tag, never :latest.
When to use secrets: only for third-party credentials (private npm registry tokens, internal artifact repo accounts). Note the in-container reference name gets a prefix — you pass { name: 'NPM_TOKEN', value: '...' } but the step references $SECRET_NPM_TOKEN, not $NPM_TOKEN; referencing a variable you didn't pass fails silently at that step with an empty value, not with a parameter error.
Example 3: full-stack in one app (one-shot deploy)
deployApp is single-service by default (Examples 1 / 2); serviceList extends it to multiple services — several services under one app name, with routes mapping paths to them. One call puts every service into the same version history:
// 0. Upload source (same as Example 2: getUploadUrl with deployType: 'custom' → PUT zip)
await manageApps({
action: 'deployApp',
serviceName: 'crm-fullstack', // one app name; frontend and backend share one version sequence
deployType: 'custom',
source: { type: 'zip', cosTimestamp: unixTimestamp }, // same as Example 1: the unixTimestamp from getUploadUrl
serviceList: [ // omit = single service (Examples 1 / 2)
{ serviceType: 'static-hosting', serviceName: 'web',
buildContext: { path: 'frontend', outPut: 'dist' }, // build dir and artifact dir for this service
command: { installCmd: 'npm ci', buildCmd: 'npm run build' } },
{ serviceType: 'http-function', serviceName: 'api',
buildContext: { path: 'backend' } },
],
routes: [ // omit = default routing
{ source: '/api', serviceType: 'http-function', serviceName: 'api' },
{ source: '/', serviceType: 'static-hosting', serviceName: 'web' },
],
});
// Polling and logs same as Example 1 (getAppVersion / getBuildLog)
Field notes: serviceType enum is static-hosting / http-function / function; routes[].source is a path (e.g. /api); targetPath is not supported yet.
Formal release: for a single-service app, commit the config into cloudbaserc.json and the release pipeline deploys automatically; for multi-service (serviceList), there are no declarative fields yet — the pipeline calls the manager SDK or a CLI script with the same parameters. Either way, the AI can assemble the deploy config in the chat; it goes through git review before release.
A knowledge note for the AI: models don't declarative-deploy by default, and can't write cloudbaserc.json from memory. To let the AI set up the pipeline itself, give it two documents: the config spec (v2.1) and declarative deploy — the skill's "minimal declarative-deploy knowledge" is the minimal set.
Version semantics
Deploying the same serviceName again adds a version, it doesn't overwrite. Chat-triggered trial deploys and pipeline-triggered releases never step on each other; version history stays continuous. For frontend assets use app, not hosting: a CloudApp serviceName gets its own subdomain, while hosting shares the environment domain and separates apps by subpath — easy to overwrite each other.
Step 5: verify
Use a standard MCP client (fake credentials + self-check skipped) to verify the protocol layer, and real credentials to verify the scope once:
initializehandshake succeeds (stdout unpolluted by logs)tools/listmatches the plugin set- Every blocked action returns
isError: true auth(action="status")returnscredential_scope=single_env— seeingaccountmeans it fell back to a global login; stop and investigate- One real deployment; the domain is reachable
One hard rule across all steps: API names, parameters, defaults, and behaviors come from docs and real tests, never from memory. When unsure, check the API index and official docs first; inferred values must be marked and verified before use — this applies to the AI too; write it into the instructions you give it.
What changes in the remote shape
The wrapper code is the same source: the factory, plugin trimming, dangerous-action interception, and audit logic are all reused. Only four things change:
| Difference | Local | Remote | Lesson |
|---|---|---|---|
| Transport | StdioServerTransport | StreamableHTTPServerTransport in stateless (short-connection) mode: each request handled independently, no session affinity | Build a fresh pair of server instance and transport per request — SDK hard constraint: one instance binds one transport at a time; concurrent reuse throws "Already connected to a transport". Under short connections, per-request construction satisfies it naturally |
| Tool surface | You trim (pluginsEnabled + filtering) | Set cloudMode: true and the official build skips registering tools that depend on local files (declarative deploy, local upload); some actions are restricted | The remote tool blocklist isn't yours to maintain — it's built in |
| Employee → wrapper | No auth (local process) | Corporate SSO / OAuth gateway | The gateway authenticates and swaps credentials; employees hold nothing end to end |
| wrapper → CloudBase | API Key (env-injected; MCP exchanges for temporary credentials) | OAuth 2.1 (browser login, keyless) or static-credential headers (X-TencentCloud-SecretId / X-TencentCloud-SecretKey — two header slots, no STS Token slot) | OAuth fits interactive clients; headers fit server-to-server |
To enable: createCloudBaseMcpServer({ cloudMode: true, ... }), swap the transport for HTTP, everything else unchanged.
A starting checklist for when you actually build it (only what existing material supports):
- Skeleton: keep the Step 2 wrapper code as-is, swap
StdioServerTransportforStreamableHTTPServerTransport(stateless mode), and construct a fresh pair of transport and instance per request - Tool surface: add
cloudMode: true— the official build narrows it automatically; keep your owntools/listfiltering for corporate-level trimming - Auth: first pick the employee → wrapper channel (corporate SSO / OAuth gateway), then the wrapper → CloudBase channel (OAuth 2.1 or static headers)
- Reference behavior: the official hosted remote MCP endpoint (
tcb-api.cloud.tencent.com/mcp/v1) is your reference — its tool surface, auth flow, and error semantics are the established facts of the remote shape
Stateless mode needs no session management — no connection lifecycle, no reconnects, no session stickiness, and horizontal scaling carries no state. Only when you need server-side long-lived sessions (e.g. SSE streaming) do you introduce a session layer; add it then.
Going further
- The full dangerous-action blocklist: the official confirmation list can serve wholesale as the denylist (function deletion, collection deletion, MySQL destruction, hosting deletion, etc.) — see Connection modes · dangerous operations
- Finer permissions: API Key whitelist mode (capability × read/write) combined with wrapper-level tool filtering; other credential shapes (authorization codes, STS) in Other credential methods
- Reserved capability:
deployTypehas a reservedhttp-functionvalue; once open, function-style deploys can be managed through the same CloudApp flow