Bỏ qua để đến nội dung

Architecture

Nội dung này hiện chưa có sẵn bằng ngôn ngữ của bạn.

This page is for people who want to understand the internals — either to self-host with confidence, debug an issue, or contribute. If you just want to connect Claude, the Quick start is enough.

Everything ships as a single Cloudflare Worker. There are exactly two public HTTP entry points:

  • /mcp — JSON-RPC over Streamable HTTP. Where Claude (and mcp-remote, and any future MCP client) sends tools/list and tools/call.
  • /admin — server-rendered HTML + a JSON /admin/api/* surface. Where a human configures sites, mints tokens, and reviews the audit log.

A handful of supporting endpoints exist alongside them — /.well-known/oauth-*, /oauth/authorize, /oauth/token, /oauth/revoke, /oauth/register — but they only support the two main surfaces.

Claude / MCP client Browser (admin)
│ │
│ Authorization: Bearer │ admin_session cookie
│ (oat_* OAuth or sn_prod_* │ (HMAC of MASTER_TOKEN)
│ bearer) │
▼ ▼
┌──────────────────── Cloudflare Worker ───────────────────────┐
│ src/index.ts (router) │
│ ├─ /mcp → src/mcp/handler.ts │
│ ├─ /oauth/* → src/auth/oauth/*.ts │
│ ├─ /admin → src/admin/router.ts (HTML SSR) │
│ └─ /admin/api/* → src/admin/api/*.ts (JSON) │
│ │
│ Auth │
│ ├─ src/auth/mcp-auth.ts (OAuth + bearer + scope) │
│ └─ src/auth/admin-auth.ts (master token + HMAC cookie) │
│ │
│ MCP dispatcher │
│ 1. resolve token → user_sites whitelist │
│ 2. tools/list = built-in 32 + proxy tools (per site) │
│ 3. tools/call: scope check → adapter → audit_log row │
│ │
│ Adapters │
│ ├─ src/adapters/wordpress.ts (REST, posts) │
│ ├─ src/adapters/duda.ts (REST, posts) │
│ ├─ src/adapters/page-wordpress.ts (REST, pages) │
│ ├─ src/adapters/page-elementor.ts (template duplicate) │
│ ├─ src/adapters/page-duda.ts (Duda Pages v2) │
│ ├─ src/adapters/page-code.ts (code-first pages) │
│ ├─ src/adapters/wp-mcp-proxy.ts (MCP-over-MCP) │
│ ├─ src/adapters/github.ts (Git Data API) │
│ └─ src/adapters/cf-pages.ts (Cloudflare Pages API) │
└──────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
D1 (metadata) Worker Secrets External APIs
+ encrypted - MASTER_TOKEN - WP REST
blobs - DUDA_API_* - api.duda.co
- GITHUB_APP_* - api.github.com
- CLOUDFLARE_* - WP MCP plugin endpoints

The worker uses a single D1 database (blog_mcp by convention — kept for backward-compat with the original project name). Migrations live in migrations/ and apply in order.

-- sites: one row per site Claude can touch
CREATE TABLE sites (
id TEXT PRIMARY KEY, -- slug e.g. "blog-acme"
name TEXT NOT NULL,
platform TEXT NOT NULL, -- wordpress | duda | astro
base_url TEXT NOT NULL,
duda_site_name TEXT, -- Duda only
wp_username TEXT, -- WP only
wp_app_password_enc TEXT, -- AES-GCM blob (0002)
page_builder TEXT DEFAULT 'gutenberg',
available_builders TEXT, -- JSON array (0008)
default_builder TEXT, -- (0008)
mcp_backend TEXT DEFAULT 'default',
mcp_endpoints_json TEXT,
github_repo TEXT, -- "owner/repo" (0009)
github_branch TEXT,
github_path_prefix TEXT,
cf_pages_project TEXT, -- (0012)
cf_pages_url TEXT,
astro_project_root TEXT, -- (0012)
notes TEXT,
created_at INTEGER NOT NULL,
last_used_at INTEGER
);
-- tokens: bearer auth + per-token scope
CREATE TABLE tokens (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
token_hash TEXT NOT NULL UNIQUE, -- sha256(plaintext)
allowed_sites TEXT NOT NULL, -- JSON array, ["*"] = all
allowed_tools TEXT NOT NULL, -- JSON array, ["*"] = all
owner_user_id TEXT, -- NULL = super-admin issued (0005)
created_at INTEGER NOT NULL,
last_used_at INTEGER,
revoked_at INTEGER
);
-- audit_log: every tool call
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts INTEGER NOT NULL,
actor_user_id TEXT, -- (0005)
token_id TEXT,
site_id TEXT,
tool TEXT NOT NULL,
args_json TEXT, -- truncated to ~1KB
status TEXT NOT NULL, -- ok | denied | error
error TEXT,
duration_ms INTEGER,
via TEXT -- "default" | "wp-mcp-adapter"
);
CREATE TABLE users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, -- PBKDF2(SHA-256, 100k rounds)
created_at INTEGER NOT NULL,
last_login_at INTEGER
);
-- M:N — which sites can each user see / mint tokens for
CREATE TABLE user_sites (
user_id TEXT NOT NULL,
site_id TEXT NOT NULL,
PRIMARY KEY (user_id, site_id)
);
CREATE TABLE oauth_clients (
client_id TEXT PRIMARY KEY,
client_secret TEXT, -- nullable for public clients
redirect_uris TEXT NOT NULL, -- JSON array
metadata TEXT NOT NULL, -- JSON DCR record
created_at INTEGER NOT NULL
);
CREATE TABLE oauth_codes (
code TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
user_id TEXT NOT NULL,
redirect_uri TEXT NOT NULL,
code_challenge TEXT NOT NULL, -- PKCE S256
expires_at INTEGER NOT NULL
);
CREATE TABLE oauth_access_tokens (
token_hash TEXT PRIMARY KEY, -- sha256(oat_*)
client_id TEXT NOT NULL,
user_id TEXT NOT NULL,
scope TEXT NOT NULL, -- "mcp:full"
expires_at INTEGER NOT NULL, -- ~1h
revoked_at INTEGER
);
CREATE TABLE oauth_refresh_tokens (
token_hash TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
user_id TEXT NOT NULL,
scope TEXT NOT NULL,
expires_at INTEGER NOT NULL, -- ~30d, rotated on use
revoked_at INTEGER
);
-- migration 0011 — one row per GitHub App installation
CREATE TABLE github_installations (
installation_id INTEGER PRIMARY KEY,
account_login TEXT NOT NULL, -- e.g. "supportsn"
account_type TEXT NOT NULL, -- User | Organization
installed_at INTEGER NOT NULL,
installed_by TEXT -- user_id who triggered install
);

See migrations/0001_init.sql through migrations/0013_*.sql for the full history. Migrations are idempotent and tracked in D1’s internal d1_migrations table — wrangler d1 migrations apply is safe to re-run.

1. Read Authorization: Bearer <token>
- If "oat_..." → look up in oauth_access_tokens, check expiry/revoke
- If "sn_prod_*"→ sha256 → look up in tokens table
- Neither match → 401 + WWW-Authenticate pointing at the OAuth metadata
2. Resolve actor:
- OAuth: user_id from oauth_access_tokens.user_id
- Bearer: tokens.owner_user_id (NULL for super-admin-issued)
3. Parse JSON-RPC frame (initialize, tools/list, tools/call, ...)
4. tools/list:
a. Built-in 32 tools (some hidden when env secrets missing)
b. For each in-scope site where mcp_backend='wp-mcp-adapter':
- Fetch tools from each MCP endpoint (cached 60s)
- Filter destructive proxy tools via proxy-guardrails.ts
- Inject required `site_id` field into each tool's input schema
c. Filter by token's allowed_tools
5. tools/call:
a. Look up tool definition (built-in or proxy)
b. Resolve site_id from args; check user_sites + token's allowed_sites
c. Resolve credentials (D1 encrypted blob + env secrets)
d. Run adapter; catch AppError / Forbidden / Upstream
e. Write audit_log row with status ok | denied | error
6. waitUntil: bump tokens.last_used_at and sites.last_used_at
1. GET /admin/login → SSR HTML form (master token OR username/password tabs)
2. POST /admin/login:
- Master tab: timingSafeEqual(body.token, env.MASTER_TOKEN)
- User tab : verify username + PBKDF2 hash from users table
- On success → set admin_session = HMAC(MASTER_TOKEN, "session:" + user_id + expiry)
3. Any other route: verify cookie HMAC → redirect to /admin/login on fail
4. Render context distinguishes "super-admin" (master) vs "user"
- Super-admin: every page, every API
- User: only own sites, own tokens, own audit rows
5. /admin/api/* responds JSON; the rest is HTML+HTMX+Tailwind CDN
1. /oauth/register (DCR) → mint client_id (+ optional client_secret), insert row
2. /oauth/authorize → SSR login form; on POST verify user, write oauth_codes
3. /oauth/token → exchange code (PKCE verify) or refresh; emit oat_*
4. /oauth/revoke → set revoked_at on both access + refresh row
5. /.well-known/oauth-protected-resource and /oauth-authorization-server
→ static JSON metadata so MCP clients can autodiscover

Worker secrets are set with wrangler secret put and never appear in source or in D1.

KeyPurposeRequired
MASTER_TOKENLogin to /admin. Also doubles as the HMAC key for admin_session cookies.yes
DUDA_API_USER / DUDA_API_PASSDuda REST API credentials. One pair per worker (shared across all Duda sites).only if you use Duda
GITHUB_APP_IDGitHub App numeric ID.only for markdown sync + Astro tools
GITHUB_APP_SLUGApp URL slug, e.g. seo-navigator-mcp.only for markdown sync + Astro tools
GITHUB_APP_CLIENT_ID / GITHUB_APP_CLIENT_SECRETOAuth credentials for the App itself (used by /admin/github install dance).only for markdown sync + Astro tools
GITHUB_APP_PRIVATE_KEYPEM string used to sign JWTs that mint short-lived installation tokens.only for markdown sync + Astro tools
CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKENLets check_astro_deploy query the Pages deployments API. The tool is hidden from tools/list if these are not set.only for Astro build verification
WP_APP_PASS__<site_id>Legacy fallback for the WordPress App Password. Normally not needed — the admin site form stores app passwords AES-GCM-encrypted in D1.no

Every site platform implements a common interface — kept loose because Duda and WordPress disagree on what “schedule” means.

// src/adapters/types.ts (simplified)
export interface PlatformAdapter {
ping(creds): Promise<PingResult>;
createDraft(creds, input: DraftInput): Promise<PostRef>;
updateDraft(creds, postId, patch): Promise<PostRef>;
scheduleDraft(creds, postId, when: Date): Promise<PostRef>;
unschedule(creds, postId): Promise<PostRef>;
getPost(creds, postId): Promise<Post>;
listDrafts(creds, opts): Promise<Post[]>;
uploadMedia(creds, input): Promise<MediaRef>;
listTaxonomies?(creds, type: TaxType): Promise<Taxonomy[]>;
createTaxonomy?(creds, type, name): Promise<Taxonomy>;
}

The proxy adapter (wp-mcp-proxy) doesn’t implement this interface — it forwards JSON-RPC frames verbatim to the remote WP-MCP server and applies the shared guardrail before returning.

The Astro / GitHub flow doesn’t use a PlatformAdapter either; those tools call adapters/github.ts directly because there’s no concept of “draft” or “schedule” — every commit is a publish to a preview branch, and Cloudflare Pages decides whether to deploy it.

Three layers of defense:

  1. Tools that don’t exist. There is no delete_post, no publish_post. The MCP tools surface refuses to call something it doesn’t expose.
  2. Adapter-level checks. wordpress.ts rejects any payload with status: "publish". duda.ts rejects POST /publish without a future schedule_publish_date. Scheduled time must be >= 60 seconds in the future to block “schedule now” trickery.
  3. Proxy guardrails. src/mcp/proxy-guardrails.ts filters proxy tool names matching (delete|remove|trash|destroy) on a page/post/template/content/media, plus any publish-* (except unpublish-*). Filtering happens in BOTH tools/list and tools/call, so even a stale tool catalog can’t bypass.

The single documented exception is delete_astro_route — see src/mcp/tools.ts around line 1716. Astro .astro files are code in a git repo, not “content” in the CMS sense; git history preserves them, and the tool is gated to super-admin tokens at runtime (tokenOwnerUserId === null).

FileWhy look at it
src/index.tsTop-level router; trace any request from here
src/mcp/handler.tsStreamable HTTP MCP server; auth, dispatch, audit
src/mcp/tools.tsDefinitions for all 32 built-in tools — single source of truth
src/mcp/proxy-guardrails.tsRegex that decides which proxy tools are forbidden
src/mcp/dynamic-tools.tsDiscovers + caches proxy tools from wp-mcp-adapter
src/auth/mcp-auth.tsOAuth + bearer parser; scope check helpers
src/auth/oauth/*DCR, authorize, token, revoke endpoints
src/admin/router.tsHTML SSR for the dashboard
src/admin/api/*JSON endpoints called by HTMX from the dashboard
src/adapters/page-code.tsThe CSS auto-scope rewriter for code-first pages
src/adapters/page-elementor.tsTemplate duplicate + placeholder replacement
src/utils/astro.tsRoute validation + HTML→Astro wrapper
src/utils/errors.tsAppError, ScopeDeniedError, UpstreamError
wp-plugins/seo-navigator-code-page/WordPress plugin needed for code-first pages

The worker fits comfortably in the Cloudflare free tier for solo and small- agency usage. Bottlenecks worth knowing about:

  • D1 row reads: every MCP call does 1–3 reads (token lookup + site + maybe user_sites). 100k reads/day = roughly 30k tool calls.
  • Subrequest budget: each tools/call makes at least one outbound fetch to WP / Duda / GitHub. Free plan allows 50 subrequests per worker request, so deploys that fan out to many APIs (e.g. deploy_md_to_site with tag creation) can approach the limit.
  • D1 write throughput: audit_log is the chattiest table. If you run a bulk scripted workload, prune it periodically from /admin/audit.

For a self-host walkthrough, see Deploy to Cloudflare. For TDD conventions before contributing, the docs/testing.md file in the code repo is the source of truth.