Skip to content

Design

The MCP server sits between AI agent clients and GitHub, serving two categories of data through fundamentally different paths.

PlantUML diagram

The token undergoes a two-stage transformation:

PlantUML diagram

This obfuscation hides the fact that the server relies on GitHub from callers. The token format is opaque to clients — they only know it as “the MCP server’s auth token.”

ToolData SourceCacheGitHub Calls
list_skillsBundled JSONN/A0
list_agentsBundled JSONN/A0
list_docsBundled JSONN/A0
get_skillGitHub API10 min TTL1
get_skill_fileGitHub API10 min TTL1
get_agentGitHub API10 min TTL1
get_docGitHub API10 min TTL1-2 (fallback)

PlantUML diagram

Cloudflare ResourceUsageTier
WorkerRequest routing, static index servingFree (100K req/day)
Durable ObjectMCP session state, SSE connection managementFree tier includes DO
Cache APIPer-colo caching of get_* responses (10 min TTL)Free (no storage limits)
Worker bundle~50-100 KB (source + bundled JSON indexes)Free (max 10 MB)
CPU timeJSON parse/serialize, Base64 decode — well under 10ms/invocationFree tier limit: 10ms

No KV, R2, D1, or Queues are used. The entire server runs within the free tier.

  • Build time (GitHub Actions): Clone source repos, generate index JSON files, bundle into Worker, deploy via wrangler deploy.
  • Runtime (Cloudflare edge): Worker receives HTTP requests, Durable Object manages MCP sessions. list_* tools return bundled data. get_* tools fetch from GitHub (with caching).
  • No origin server: The Worker is the entire backend. There is no origin, database, or long-running process.

PlantUML diagram

The testing strategy separates pure logic (testable in isolation) from platform integration (verified manually post-deploy).

LayerTest FileMock StrategyCoverage
Auth decodingauth.test.tsNone (pure function)URL-safe Base64 decode, missing header, invalid Base64, non-Bearer prefix
Path validationvalidation.test.tsNone (pure function).. traversal, absolute paths, backslashes, valid paths pass
GitHub clientgithub.test.tsGlobal fetch mock returning canned GitHub API responsesURL construction, content decoding, error mapping (401/403/404/5xx), index.md fallback logic
Cache wrappercache.test.tsIn-memory Map replacing Cache APICache hit returns stored value, miss triggers fetch, skipCache bypasses and refreshes, key construction
Tool handlerstools.test.tsfetch mock + cache mock + fixture indexeslist_* returns bundled data with zero fetch calls, get_* integrates auth + validation + github + cache
ConcernWhy NotHow Verified
McpAgent Durable Object lifecycleRequires Cloudflare runtime — cannot be fully simulated in VitestManual smoke test: connect MCP client to deployed Worker
SSE transportCloudflare-specific HTTP streamingManual: verify SSE stream opens and tool calls succeed
wrangler deployInfrastructure concernCI job success + manual verification
Index generation scriptsRun as Node.js scripts against cloned reposTested implicitly by CI pipeline (generate → build → deploy); can also run locally

fetch mock (GitHub API):

// Vitest global mock returning typed responses
vi.stubGlobal('fetch', vi.fn())
function mockGitHubSuccess(content: string, sha = 'abc123') {
return new Response(JSON.stringify({
content: btoa(content),
sha,
size: content.length,
encoding: 'base64',
type: 'file',
name: 'SKILL.md',
path: 'instructions/claude/skills/test/SKILL.md',
}))
}
function mockGitHubError(status: number) {
return new Response('', { status })
}

Cache API mock:

// In-memory cache for testing
const cacheStore = new Map<string, { response: Response; expiry: number }>()
const mockCache = {
match: async (key: string) => {
const entry = cacheStore.get(key)
if (!entry || Date.now() > entry.expiry) return undefined
return entry.response.clone()
},
put: async (key: string, response: Response) => {
cacheStore.set(key, { response: response.clone(), expiry: Date.now() + 600_000 })
},
}

Bundled index fixtures:

// Test fixture matching the generated index shape
const testSkillsIndex = [
{ name: 'test-skill', description: 'A test skill', argumentHint: null, userInvocable: false, files: [] },
{ name: 'with-files', description: 'Skill with extras', argumentHint: '[arg]', userInvocable: true, files: ['examples/demo.ts'] },
]

PlantUML diagram

TriggerWhenPurpose
push to mainPR mergeDeploy changes to the MCP server code itself
schedule (cron)Saturday 00:00 Pacific (07:00 UTC)Pick up changes in source repos (skills, agents, docs)
workflow_dispatchManualOn-demand rebuild for urgent updates
repository_dispatchFutureDirect trigger from source repo pipelines on merge
NameTypeWhere UsedPurpose
REPO_ACCESS_TOKENSecretIndex generation stepGitHub PAT to clone source repos (never in Worker bundle)
CLOUDFLARE_API_TOKENSecretDeploy stepAuthenticate wrangler deploy to Cloudflare
ARDA_GH_ACTION_PROJECT_WRITERSecretPR upkeep workflowAdd PRs to GitHub Projects board
REPOVariableIndex generation stepArda-cards/agentic-workspace
DOCS_REPOVariableIndex generation stepArda-cards/documentation
SKILLS_PATHVariableIndex generation stepinstructions/claude/skills
AGENTS_PATHVariableIndex generation stepinstructions/claude/agents
DOCS_CONTENT_PATHVariableIndex generation stepsrc/content/docs

The pipeline has a strict separation between build-time and runtime secrets:

  • Build-time: REPO_ACCESS_TOKEN clones source repos. This token is used only in the GitHub Actions runner and is never embedded in the Worker bundle.
  • Runtime: The Worker has no secrets. The caller’s PAT arrives in the HTTP request header, is decoded, used for the GitHub API call, and discarded. It is never stored in Durable Object state, KV, or logs.