unofficial-vwo-mcp-server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@unofficial-vwo-mcp-serverlist my active campaigns"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
unofficial-vwo-mcp-server
An unofficial MCP server that exposes VWO platform
operations as tools an AI agent can call. Built on the MCP TypeScript SDK v2
(@modelcontextprotocol/server), stdio transport.
Not affiliated with or endorsed by VWO / Wingify.
New to this repo? See GETTING-STARTED.md for a step-by-step walkthrough (clone, build, get a token, register with a host). This file is the deeper reference — why things work the way they do.
Tools
45 tools. Reads are unmarked; ⚠ marks a tool that mutates VWO state and carries
REQUIRES_HUMAN_APPROVAL, so hosts prompt a person on every call.
Area | Tools |
Diagnostics |
|
Workspaces |
|
Campaigns |
|
Drafts |
|
Goals |
|
Variations |
|
Sections |
|
Metric reports |
|
Labels |
|
Tracking code |
|
Custom widgets |
|
Every tool name carries a vwo_ prefix. Claude Code additionally namespaces by server
(mcp__vwo__vwo_list_campaigns), but the local prefix is what survives in hosts that
don't namespace, and in any text — tool descriptions, error messages — that only ever
shows the bare name.
Uses VWO REST API v2 exclusively. v1 is retired and the base URL is validated to
end in /v2 at startup — see API version safety.
Where the docs and the live API disagree
Every route was verified against the live API (a 401 proves the route exists; a 404
proves it does not). Three cases where following the reference literally would have
produced broken or wrong tools:
Operation | Reference says | Actual |
Update/delete a campaign goal |
| Singular 404s. Plural |
Custom widgets | — | Served from |
Update bulk custom widgets |
| Unrelated endpoint. See caveat below. |
vwo_update_custom_widgets is the one unverified endpoint. Its doc page points at an
attribute-list endpoint that has nothing to do with widgets, and the nav lists the
operation as GET. This server uses PATCH /accounts/{a}/changesets/bulk, which exists
and is consistent with the single-widget PATCH and the bulk POST. The tool's own
description tells the agent the endpoint is inferred. Confirm the first real call.
Collection response shapes are not consistent
VWO returns list collections in two different shapes, on the same endpoint depending on parameters. Verified against the live API:
Endpoint |
|
|
|
| flat array |
|
|
| flat array |
|
|
_metadata is never present on any of them, despite the docs implying it; the real
total lives at _data.totalCount, and only on the wrapped shape.
This bit hard: an earlier listResult handled only the flat-array case and silently
returned [] for the wrapped one, so a workspace holding 142 campaigns reported
count: 0. extractCollection in src/tools/shared.ts now
normalizes every shape above, and — because the failure was silent, which is what made
it dangerous — an unrecognized shape now surfaces a warning in the tool result telling
the agent to report a bug rather than conclude there is no data.
Two more undocumented /campaigns behaviors
status is a real query parameter that VWO does not document. UPPERCASE only
(lowercase returns HTTP 400). Valid values, taken from the error message VWO returns for an
invalid one: ACTIVE, DELETED, ARCHIVED, RUNNING, PAUSED, STOPPED, NOT_STARTED.
Omitting it returns campaigns of every status — including soft-deleted ones, so check each
result's own isDeleted and status fields before reporting a campaign as live. Note
status=ARCHIVED returns campaigns whose own status field reads STOPPED: archived-ness
is a separate axis from the status value.
limit is capped at 25 on this endpoint, silently — limit=50 and limit=100 both
return 25 (while /feeds honors limit=100, so the cap is per-endpoint). The tool's schema
caps limit at 25 to match, because allowing a larger value broke paging: nextOffset is
derived partly from whether a page came back full, and a 25-item response to a limit=100
request looks like a final page. Verified after the fix that paging walks all 142 campaigns
across 6 pages and terminates correctly.
Two more shapes worth knowing: vwo_update_campaign_status is a bulk endpoint with no
campaign id in the path (ids go in the body), and vwo_update_campaign requires its payload
wrapped in a campaigns object — the tool adds that wrapper itself, since models
reliably get nesting like that wrong.
Campaign type/platform values are lowercase and hyphenated, not the generic
A/B-testing names they resemble. An early version of this server's tool descriptions
said "AB", "SPLIT_URL", "MVT", "FUNNEL", "WEB", "FULLSTACK" — all wrong. VWO's
own type filter enum on GET /campaigns is ab, multivariate (not mvt), split
(not split-url), feature-rollout, feature-test, plus non-testing types (heatmap,
survey, recording, …); platform is website, full-stack, or mobile-app. Fixed in
src/tools/campaigns.ts and reflected in the workflow prompts
below, which is also how feature-rollout was confirmed as the real type value behind
"Web Rollouts."
Request bodies
VWO documents a request schema for only four write endpoints (vwo_new_workspace,
vwo_update_workspace, vwo_new_campaign, vwo_update_campaign). Those get explicit typed fields.
Every other write tool takes a validated body object passed through to VWO, with the
relevant doc URL in the tool description — rather than a strict schema invented here that
would reject valid payloads.
Related MCP server: VoIPbin MCP Server
Prompts
Tools are actions an agent takes; prompts are a separate MCP primitive for guidance — text that steers how the model approaches a situation, with no tool call attached. This server exposes four:
Prompt | Purpose |
| House rules that apply everywhere: resolving workspaces, what needs approval, rate limits, which workflow prompt to use, and to check VWO's own docs for platform-behavior questions. No arguments. |
| Inspect → plan → apply → verify → iterate for a same-page A/B test (type |
| The same shape, for a Split URL test (type |
| The same shape, for a Web Rollout (type |
All three workflow prompts do real work before returning: when given a campaignId, each
fetches that campaign and its variations server-side — including, for the A/B and rollout
workflows, each variation's editorData (VWO's undocumented field for the actual DOM/JS/CSS
a variation applies) — and embeds that snapshot directly in the returned guidance, so the
model starts from real data instead of spending a turn discovering it. If the fetch fails
for any reason (bad token, wrong id, ambiguous workspace), the prompt still returns
successfully — it degrades to telling the model to fetch that information itself.
The workflow each prescribes, in short: for an existing campaign, understand what's there
in your own words before touching anything (either its code, for A/B/rollout, or which URL
each variation points to, for split); for a brand-new one, run a full requirements
checklist first (below) since this server has no delete_campaign tool — once
vwo_new_campaign succeeds there's no undoing it through this API. Either way: plan the
specific change and say so before calling a write tool; apply one focused change; then
verify it visually — see Verifying a change in a browser
below — before considering it done; iterate using user feedback, with a cap so it doesn't
loop blindly forever. Shared machinery (snapshot fetch, verify/iterate text, wrap-up) lives
in src/prompts/shared.ts; read
src/prompts/abTestWorkflow.ts and its two siblings for
the exact text.
New-campaign checklists. Nothing gets inferred silently except items with a stated
default. Shared by all three: workspace, page targeting (urls/excludedUrls — explicitly
confirmed, never silently assumed to be "just this one page"); audience defaults to All
Visitors with no segment filtering unless stated otherwise, stated explicitly in the plan
so it's easy to override. Then per campaign type:
A/B test: campaign type asked if ambiguous between
ab/split/multivariate(real VWO values — see type value corrections below); at least one goal required; traffic split defaults even across variations; variation count and what each one does are extracted from the request if implied, or asked per-variation if vague.Split test: type is fixed at
split(stated, not asked, since invoking this prompt already answers that); each variation needs its own explicit destination URL — never invented; still requires goals, same as an A/B test (a split test is still a controlled experiment). One genuinely open question flagged in the prompt itself: VWO's API doesn't document which field carries a variation's destination URL — the model is told to inspect what VWO actually returns rather than guess a field name, and to check VWO's support docs if that isn't enough.Web rollout: type is fixed at
feature-rollout; no goal is asked for — the prompt has the model trygoals: []first (VWO's schema shows no minimum length, though that's not a server-side guarantee) and falls back to a placeholder goal only if VWO rejects the empty array, explaining to the user why it exists; no control variation, just one, at a rollout percentage that defaults to 100% unless the user wants a staged rollout.
Prompts: what they are and their limits
Prompts are offered by the server, but nothing in MCP requires a host to do anything
with them. tools/call is universal because it's the entire point of a tool-using agent;
prompts/list and prompts/get are separate calls a host has to choose to make. Claude
Code wires prompts up as slash commands (/mcp__vwo__vwo_ab_test_workflow, and likewise
for the split-test and rollout ones), so a person can invoke one directly. Whether the
model itself ever decides to call prompts/get depends entirely on your own wrapper's
logic — nothing about registering a prompt makes an agent aware it exists unless the host
lists prompts for it or a human invokes one.
That's why the server-level instructions field (sent once, automatically, at connect
time — see src/index.ts) explicitly names all four prompts: it's the one
guaranteed way to make the model aware they exist without your wrapper doing anything
extra. If your wrapper's agent loop calls prompts/list itself and decides when to invoke
one based on the user's request, that's the more autonomous version of this — but it's
work your wrapper has to do; this server can't reach into your agent loop and invoke its
own prompt on the model's behalf.
Verifying a change in a browser
vwo_get_campaign_share_link does not return a live preview. I checked VWO's actual
response schema before writing anything that depends on it: it returns a link into VWO's
own dashboard summary/report page (https://app.wingify.com/#/campaign/{id}/summary?token=...),
and there is no separate "preview URL" endpoint anywhere in VWO's v2 API.
What VWO's product actually provides: that summary page hosts a preview control — a field to enter a URL and a button that opens a live rendering of the campaign for that URL in a new tab. That's a UI feature, not an API contract, so the prompts tell the model to locate it visually (via a screenshot) rather than assume fixed coordinates or a stable selector. The intended flow, if a browser automation tool is present in the session — the prompts check for any browser automation capability, not specifically a tool named "Chrome DevTools MCP":
Open the share link.
Find the preview control, enter the campaign's
primaryUrl, activate it.It opens a new tab with the variation actually rendered — enumerate open tabs to find it, switch to it, screenshot it, and compare against the specific intended change (for the split-test workflow: compare against the intended destination URL instead — there is no DOM to diff, and VWO's traffic split may route a given visit to any variation, so one preview attempt confirms landing on one expected destination, not all of them at once).
On a further edit, reload that same tab rather than reopening the share link and re-clicking through the control each time.
If no browser tool is available at all, the prompt tells the model to say so explicitly
and immediately — not discover the gap silently after attempting a call, and not quietly
downgrade to a weaker check while implying the change was verified. It then tries, in
order: ask the user to open the share link and preview control themselves and report what
they see (their report counts as verification); failing that, fall back to re-fetching the
variation to confirm editorData stored the intended edit — and explicitly label that
outcome unverified, since confirming the data was stored is not the same as confirming
it rendered. The point is that the model should never report a content change as "done"
with more confidence than it actually earned.
Quick start
Requires Node.js >= 20.19 (declared in package.json's engines field).
npm install && npm run buildThen set a token and confirm it works — this makes one real API call and prints a diagnostic:
VWO_API_TOKEN=your-token-here node dist/index.js --verifyExit code 0 means the credentials work. Generate a token at
https://app.vwo.com/#/developers/tokens.
To actually register the server with Claude Code and/or Claude Desktop, see
GETTING-STARTED.md — node scripts/register_mcp_server_claude.js does
the build, the link, and the host config in one interactive pass.
Configuring the API token
This is the part worth getting right, so here is how MCP servers handle it in general before the specifics.
The model never sees the token, because the token is never part of the MCP
conversation. An MCP server is a separate process. The host (your agent wrapper)
launches it and passes secrets through the process environment — the same way you'd
configure any CLI tool. The agent only ever sees tool results. There is no code path
by which the token reaches the model's context: it is read once at startup in
src/config.ts, attached to outbound requests in
src/vwo/client.ts, and scrubbed from every log line and tool
result by src/redact.ts.
That is the standard pattern — the official GitHub, Slack, and Postgres MCP servers all work this way.
Option A — .mcp.json in the consuming project, with variable expansion (recommended)
Drop examples/mcp.json into the root of the project that uses the
server, as .mcp.json. Claude Code expands ${VAR} and ${VAR:-default} in command,
args, env, url, and headers — so the file references the secret without
containing it, and stays safe to commit:
{
"mcpServers": {
"vwo": {
"type": "stdio",
"command": "node",
"args": ["${VWO_MCP_HOME:-../unofficial-vwo-mcp-server}/dist/index.js"],
"env": {
"VWO_API_TOKEN": "${VWO_API_TOKEN}"
}
}
}
}VWO_API_TOKEN then comes from wherever you keep it — your shell profile, your OS
keychain, or .claude/settings.local.json (below). If it's unset, Claude Code loads the
config anyway and reports a missing-variable warning in claude mcp list.
Project-scoped .mcp.json servers need one-time approval; Claude Code prompts on first
use, or you can pre-approve with "enabledMcpjsonServers": ["vwo"] in
.claude/settings.json.
Scopes. --scope project writes .mcp.json (shared, committed). --scope local —
the default — writes ~/.claude.json under that project's path, which is private to you
and never in version control. Local scope is the better home for a literal token:
claude mcp add vwo --scope local --env VWO_API_TOKEN=your-token -- node /abs/path/dist/index.jsOption A2 — .claude/settings.local.json to supply the variable
.claude/settings.json supports an env block whose variables apply to the session and
to subprocesses Claude Code spawns, which includes MCP servers. Put the secret in
.claude/settings.local.json — Claude Code gitignores that file when it creates it, and
it overrides the committed settings.json:
{
"env": {
"VWO_API_TOKEN": "your-token-here"
}
}Pair it with the committed .mcp.json from Option A: the shared file declares the
server, the personal file supplies the credential. If you create
settings.local.json by hand, add it to .gitignore yourself.
Most hosts work the same way — a command/args/env block — but the exact file and key
names vary. Two more, each verified against its current docs rather than assumed:
VS Code (native MCP support). Save examples/vscode-mcp.json
as .vscode/mcp.json (workspace) or via the MCP: Open User Configuration command
(user profile, applies to all workspaces). The top-level key is servers, not
mcpServers. Secrets use an inputs block instead of shell expansion:
{
"inputs": [
{ "type": "promptString", "id": "vwo-api-token", "description": "VWO API token", "password": true }
],
"servers": {
"vwo": {
"type": "stdio",
"command": "node",
"args": ["/absolute/path/to/unofficial-vwo-mcp-server/dist/index.js"],
"env": { "VWO_API_TOKEN": "${input:vwo-api-token}" }
}
}
}VS Code prompts for the token the first time the server starts and stores it securely —
it's never written into mcp.json.
Codex CLI. Append to examples/codex-config.toml at
~/.codex/config.toml (all projects) or ./.codex/config.toml (this project). Codex's
config is TOML, not JSON, and has no ${VAR} expansion — instead, env_vars forwards
specific variables that are already set in your shell into the server's process:
[mcp_servers.vwo]
command = "node"
args = ["/absolute/path/to/unofficial-vwo-mcp-server/dist/index.js"]
env_vars = ["VWO_API_TOKEN"]Any other host follows the same shape — declare the server, supply VWO_API_TOKEN
however that host keeps secrets out of its config file:
{
"mcpServers": {
"vwo": {
"command": "node",
"args": ["/absolute/path/to/unofficial-vwo-mcp-server/dist/index.js"],
"env": { "VWO_API_TOKEN": "your-token-here" }
}
}
}If a token ends up literally in a config file, protect it like any credential file
(chmod 600, never committed).
Option B — a token file (better isolation)
A process's environment is readable by other processes on some systems (ps -e,
/proc/<pid>/environ). To avoid that, point the server at a file whose entire contents
are the token:
"env": { "VWO_API_TOKEN_FILE": "/run/secrets/vwo_api_token" }VWO_API_TOKEN_FILE takes precedence over VWO_API_TOKEN. This composes directly with
Docker/Kubernetes secret mounts and with pass/age-style secret files.
Option C — reading from an OS keychain at spawn time
Best of both for a desktop wrapper: keep the secret in the OS keychain and resolve it only when launching the child process, so it is never written to disk in plaintext.
// In your agent wrapper, when spawning the MCP server:
const token = await keytar.getPassword('vwo-mcp', 'api-token'); // or `security find-generic-password` / DPAPI
const child = spawn('node', ['dist/index.js'], {
env: { ...process.env, VWO_API_TOKEN: token },
stdio: ['pipe', 'pipe', 'pipe']
});Your wrapper owns the "ask the user for their key once, store it securely" UX; the MCP server stays a dumb consumer of an env var. That separation is deliberate — it keeps the server usable from any host.
Option D — .env for local development only
Copy .env.example to .env. .env is gitignored. Note the server does
not load .env itself; use your shell or a runner:
node --env-file=.env dist/index.js --verifyWhy the token is not a tool parameter
A tempting alternative is an authenticate(apiKey) tool. Don't — it is the one design
that actively breaks the security property:
The model would have to know the key to pass it, so it lands in the context window.
It would be written into conversation transcripts and any logging around them.
It would be replayed on every tool call, multiplying exposure.
Anything that can read the transcript — including a prompt injection that gets the agent to repeat it — can exfiltrate it.
Secrets belong in the process boundary, not the conversation. There is intentionally no tool in this server that accepts, sets, or returns a token.
If you later expose this over HTTP instead of stdio
For a multi-tenant or remote deployment, per-user tokens in the server environment stop
making sense. MCP's answer is OAuth 2.1: the server acts as an OAuth Resource Server and
each request carries the caller's bearer token. The SDK ships the pieces
(verifyBearerToken, OAuthTokenVerifier, protected-resource metadata helpers). That is
a different architecture from this one — out of scope here, but the client and tool layers
would carry over unchanged since auth is isolated to config.ts + client.ts.
All configuration
Variable | Required | Default | Purpose |
| yes* | — | API token. |
| yes* | — | Path to a file containing the token. Wins over |
| no | — | Default account: a numeric id, or |
| no | unrestricted | Comma-separated allow-list of account ids this server may touch. |
| no |
| Set to |
| no |
| Header carrying the token, per VWO's docs. |
| no |
| Request spacing. VWO allows 1 req/sec per token. |
| no |
| Per-request timeout. |
| no |
|
|
* One of the two is required. Startup fails with exit code 78 (EX_CONFIG) and an
actionable message if neither is set.
Which tools auto-run and which need approval
This is a host concern, not something .mcp.json controls. In Claude Code it lives in
.claude/settings.json under permissions — see
examples/claude-settings.json:
{
"enabledMcpjsonServers": ["vwo"],
"permissions": {
"allow": ["mcp__vwo__vwo_verify_connection", "mcp__vwo__vwo_list_*", "mcp__vwo__vwo_get_*"],
"ask": ["mcp__vwo__vwo_new_*", "mcp__vwo__vwo_create_*", "mcp__vwo__vwo_add_*", "mcp__vwo__vwo_update_*"],
"deny": ["mcp__vwo__vwo_delete_*"]
}
}The tool names are deliberately prefix-consistent so this stays a four-line policy:
vwo_list_* and vwo_get_* are exactly the read tools, and every mutating tool starts
with vwo_new_, vwo_create_, vwo_add_, vwo_update_, or vwo_delete_. Move
vwo_delete_* from deny to ask when you want deletions available.
The mcp__vwo__ and vwo_ prefixes look redundant here on purpose: mcp__vwo__ is
Claude Code's server namespace and disappears in hosts that don't add one, while vwo_
is this server's own prefix and is what the model actually reads in tool descriptions and
error messages ("call vwo_list_workspaces first") regardless of host. If you expect to
run this server alongside others that also prefix their own tools, the double prefix is
the price of names that stay meaningful outside Claude Code too — see multiple MCP
servers.
Matcher syntax:
Pattern | Matches |
| every tool from the |
| same, wildcard form |
| that one tool |
| its |
The server name is whatever key you used in .mcp.json. Allow-rule globs are only
permitted after a literal mcp__<server>__ prefix — a bare "*" or "mcp__*" in
allow is ignored with a warning. deny and ask accept broader globs, so
"deny": ["mcp__*"] blocks all MCP tools.
Put the rules in committed .claude/settings.json to share them, or
.claude/settings.local.json to keep them personal (local wins).
Running alongside other MCP servers
If your wrapper also loads, say, Chrome DevTools MCP, every tool name arrives in the
model's context at once. Two reasons vwo_ earns its keep in that situation specifically:
Not every host namespaces by server the way Claude Code does. A wrapper that flattens tool names, or a host with no namespacing convention at all, would otherwise expose a bare
list_campaignssitting next to some other server'slist_pageswith nothing marking either as belonging to a particular integration.Namespacing only covers the tool list. Tool descriptions and this server's own error messages reference other tools by name in prose (
"call vwo_list_workspaces first"), and that text is host-agnostic — it reads correctly whether or not the host prefixes anything.
The trade-off is that in a host which does namespace, permission rules end up with the
double prefix seen above (mcp__vwo__vwo_list_campaigns). That's cosmetic; the glob
policy is unaffected either way.
Two server-side backstops, so a permissive host config can't cause damage on its own:
Read-only tools declare
annotations.readOnlyHint, letting hosts treat them as safe.Destructive tools should spread
REQUIRES_HUMAN_APPROVALfromtools/shared.tsinto their config. Claude Code then prompts on every call even inbypassPermissionsmode and even if anallowrule matches. Use it for anything that mutates a live experiment.
Choosing which VWO account a tool acts on
Your token manages multiple accounts (VWO's UI calls them workspaces), so this is the part most likely to go wrong quietly. The resolution order for every account-scoped tool:
Explicit
accountIdargument.workspaceNameargument, resolved viaGET /accounts.VWO_ACCOUNT_IDdefault, if configured.Otherwise: a deliberate error telling the agent to call
vwo_list_workspacesand ask the user.
Step 4 is the important one. The obvious alternative — falling back to VWO's
accounts/current — is the dangerous option: a multi-account token would happily run the
call against whichever account VWO considers current, which is rarely the one the user
meant. Failing with instructions costs one extra round trip and can't touch the wrong
client's data.
So does the LLM ask, or look it up?
It looks it up, then asks only if ambiguous. Concretely, when the user says "pause the homepage test in Acme Corp":
The agent calls
vwo_list_workspaces→[{id: 12345, name: "Acme Corp"}, ...].It calls
vwo_list_campaignswithaccountId: 12345.
You can skip step 1 by passing workspaceName: "Acme Corp" directly; the server resolves it.
Name resolution is deliberately strict — an unknown or ambiguous name returns an error
listing the candidates rather than picking one, which turns a potential wrong-account
write into a clarifying question. The tool descriptions tell the model, in as many words,
never to guess an id.
The account list is cached for 60 seconds, since VWO allows only 1 request/second per token and name resolution would otherwise spend that budget on every call.
Pick the setup that matches your usage
Situation | Configuration |
Always one account |
|
Many accounts, agent chooses | Leave |
Many accounts, but only some in scope |
|
One agent per client | Run one server instance per account, each with its own |
VWO_ALLOWED_ACCOUNT_IDS is enforced in two places: the account list is filtered, and any
explicit accountId outside the list is refused. It's the guardrail worth setting if the
token can reach clients this agent has no business touching.
API version safety
The server uses v2 only. Worth knowing why that's enforced rather than assumed:
https://app.wingify.com/api/v1/... returns HTTP 200 with a body of
{"API_ERROR":"INCORRECT_API_VERSION"}. A client that trusts the status code would read
that as success and hand the agent an empty result.
Two guards:
VWO_API_BASE_URLmust end in/v2, checked at startup.The client inspects every 2xx body for VWO's error envelopes (
_errors,API_ERROR) and raises aVwoApiErrorif present. VWO returns errors in-band, so status codes alone are not trustworthy.
Project layout
src/
index.ts entry: loads config, serves stdio, handles --verify
config.ts env + token-file loading, validation, defaults
logger.ts stderr-only logging (stdout is the JSON-RPC channel)
redact.ts secret-scrubbing registry
vwo/
client.ts HTTP client: auth header, rate-limit gate, retries, error envelopes
errors.ts VwoApiError + agent-facing messages
accounts.ts account/workspace directory, name resolution, caching
verify.ts shared credential check
tools/
index.ts single registration point for all tools
shared.ts ToolContext, accountArgs, resolveAccount, bodyArg, error wrapper
campaignResource.ts factory for goals/variations/sections (identical CRUD shape)
diagnostics.ts vwo_verify_connection
workspaces.ts campaigns.ts drafts.ts
goals.ts variations.ts sections.ts
labels.ts metric_reports.ts tracking_code.ts
custom_widgets.ts websites.ts (empty — no website tools requested yet)
prompts/
index.ts single registration point for all prompts
shared.ts snapshot fetch, verify/wrap-up sections shared by the 3 workflows
general.ts vwo_general_guidance
abTestWorkflow.ts vwo_ab_test_workflow — same-page content changes
splitTestWorkflow.ts vwo_split_test_workflow — per-variation destination URLs
webRolloutWorkflow.ts vwo_web_rollout_workflow — no control, no goal
examples/
mcp.json Claude Code: drop into a consuming project as .mcp.json
claude-settings.json Claude Code: permission rules for .claude/settings.json
vscode-mcp.json VS Code: save as .vscode/mcp.json
codex-config.toml Codex CLI: append to ~/.codex/config.toml
scripts/
register_mcp_server_claude.js interactive: build + link + register with
Claude Code and/or Claude Desktop (see GETTING-STARTED.md)
install_package_locally.sh/.bat build + `npm link` only, for wiring into
a host's config by handAdding a tool
Create
src/tools/<area>.tsfollowingcampaigns.ts.Spread
accountArgsinto the input schema and callawait resolveAccount(ctx, args)so every tool targets an account identically and inherits the allow-list check.Wrap the implementation in
toolHandler(name, fn)so VWO errors become readableisErrorresults instead of exceptions.Call VWO through
ctx.client— never construct headers or read the token in tool code.For anything that mutates state, set
annotations.destructiveHintand spreadREQUIRES_HUMAN_APPROVAL.Register it in
src/tools/index.ts.
Write descriptions for the agent, not for a human reading API docs: say when to reach for
the tool and what it returns, and use annotations.readOnlyHint / destructiveHint
honestly so hosts can gate write operations.
Design notes
stdout is sacred. On stdio transport, stdout carries JSON-RPC framing. All logging goes to stderr;
console.logmust never be used in this project.Rate limiting is process-wide. VWO allows 1 request/second per token, so one shared gate in
VwoClientpaces all calls regardless of how many tools fire concurrently.Retries are opt-out for writes. GETs retry on 429/5xx honoring
Retry-After; POST/PATCH/DELETE do not retry, since VWO writes are not idempotent.Errors are written for an agent.
VwoApiError.agentMessagetells the model whether a failure is worth retrying — a 401 explicitly says "configuration problem, do not retry".
SDK version pinning
@modelcontextprotocol/server is pinned to an exact beta (2.0.0-beta.3) because v2 is
pre-stable and its API is still moving. This project's npm is configured with
min-release-age=14, so the newest installable beta lags the newest published one. Bump
deliberately:
npm view @modelcontextprotocol/server versions --json
npm install @modelcontextprotocol/server@<next-beta> && npm run buildThe surfaces used are McpServer, registerTool, and serveStdio — the most likely
breaking changes are in tool-registration and result shapes, both confined to src/tools/.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/nextafter-michael/unofficial-vwo-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server