AI Client
ChatGPT / Claude / IDE
Asks a question through an AI tool that supports MCP.
NTPU AI4X MCP lets AI assistants β ChatGPT, Claude, IDE tools, and other MCP clients β query NTPU AI4X's public information in a standardized way. It cannot browse the web freely and it cannot modify any data: every answer comes from information that has already been collected, verified, and stored.
This page moves from plain-language concepts to implementation detail. If you're new here, start with System Overview and Request Flow; engineers can continue into Tech Stack, Design Priorities, Security, Engineering Detail, and Deploy & Operate below.
How should I read this page?
30-Second Overview
ChatGPT / Claude / IDE
Asks a question through an AI tool that supports MCP.
Cloudflare Worker
Receives the MCP request and validates the tool and arguments.
Input & Schema Validation
Rejects unregistered tools and invalid arguments at the door.
Business Logic
Decides how to look up the data β the only layer allowed to call a repository.
Cloudflare D1
Structured canonical data.
Cloudflare R2
Documents and source snapshots.
Official Source
An allowlisted official data source.
Cloudflare Worker
Triggered on a daily schedule, not by any public route.
Zod + Content-Hash Diff
Converts to the canonical shape and checks whether anything changed.
Canonical Storage
Only writes a new version if the content hash actually changed.
NTPU AI4X MCP is a public, read-only Model Context Protocol server, built as two
independently deployed Cloudflare Workers: a public-facing MCP server
(ai4x-mcp-server) that answers every client request, and a separate
ingestion worker (ai4x-ingestion-worker) that is not part of the public
request path at all.
Every MCP tool call passes through the same pipeline inside the MCP worker: protocol
layer, then tool registry and Zod schema validation, then a domain service, then a D1 or
R2 repository. There is no route that bypasses tool registration or input validation.
Even the one non-MCP HTTP route, Ask AI4X, calls the exact same
domain-service functions the MCP tools call, never a second, parallel implementation of
the same lookup.
All authoritative records (center info, services, members, projects, documents, events) live in one D1 database per environment; raw source snapshots and document bodies live in a separate R2 bucket per environment. Both are private Worker bindings only; there is no public bucket URL, no generic database tool, and no URL-fetch tool anywhere in the MCP tool surface.
Three independent flows exist in this system, and none of them call each other: an MCP tool call, the optional Ask AI4X assistant, and the scheduled ingestion job that keeps D1 up to date.
For ChatGPT, Claude, IDE tools, and other MCP clients β queries the official data.
Asks a question
Receives the MCP request
Tool Registry + Zod
Decides how to get the data
Reads verified data only
Answer with provenance
AI Client (Claude / ChatGPT / IDE / custom MCP client)
β MCP over Streamable HTTP (JSON-RPC), POST /mcp
MCP Worker: Protocol Layer
handles initialize / tools/list / tools/call
β
Tool Registry + Zod Schema Validation
rejects unknown tools and invalid arguments before any code runs
β
Domain Service (packages/domain)
the only code allowed to call a repository
β
D1 Repository / R2 Repository (packages/database)
bound-parameter SQL only, no string-built queries
β
runTool() adapter: authorize() β execute β structured audit event β error mapping
β
Structured result + provenance (source, version, verifiedAt) returned to the client
For web visitors β the Ask AI4X question-and-answer box on the Playground page.
/playground page
POST /api/ask-ai4x
Picks one tool, up to 3 rounds
Same function an MCP tool calls
With source and provenance
Browser (same-origin /playground page)
β POST /api/ask-ai4x (not an MCP route, never in tools/list)
Ask AI4X Handler
β
Cloudflare Workers AI selects at most one tool per round (up to 3 rounds)
β
The selected tool's own domain-service function is called directly
(never a loopback HTTP call to /mcp)
β
Grounded answer + the same provenance a matching MCP tool call would return
+ an inspectable trace (question, selected tool, arguments, result status per round)
For system operators β keeps the canonical data current, not user-triggered.
Daily, scheduled
No public HTTP route
Nothing else can be fetched
Raw snapshot, deduplicated
Zod-checked candidate record
Only if the content hash changed
Cloudflare Cron Trigger (daily, 0 18 * * * UTC)
β
Ingestion Worker (ai4x-ingestion-worker, has no public HTTP route)
β
Fetch one allowlisted source URL (SOURCE_ALLOWLIST; nothing else can be fetched)
β
Archive the raw response to R2 first, deduplicated by content hash
β
Parse + normalize into the canonical shape for that dataset
β
Zod-validate the normalized candidate record
β
Hash the candidate and compare to the current canonical row's hash
unchanged β no-op (no write at all)
changed β bump version, close the superseded record_versions row,
upsert the canonical D1 row, insert a new record_versions row
Everything below is running in production at
https://ai4x.mcp.ntpu.ai, not a proposal under evaluation.
Every MCP tool call passes through these core components in order.
Cloudflare Workers
Receives and handles every MCP client's query.
Zod
Every tool name and input argument must pass schema validation first.
Domain Layer
Centralizes query rules β the only service layer allowed to call the data storage layer.
Cloudflare D1
Stores structured canonical data for center info, services, members, projects, and events.
Cloudflare R2
Keeps source snapshots, document content, and other unstructured data.
These components handle data updates, AI Q&A, and system auditing.
Ingestion Worker
Fetches from allowlisted official sources and updates canonical data daily.
Cloudflare Workers AI
Provides the Playground's optional Q&A feature, sharing the same data and domain logic as MCP.
Cloudflare Workers Logs
Logs structured events for tool calls, trace ids, latency, results, and status.
This service answers questions about a real institution. When design decisions conflict, they are resolved in this order:
1
Verifiable sources
An answer without a verifiable source is never presented as fact: the system returns
an explicit NO_AUTHORITATIVE_SOURCE error instead of guessing.
2
Read-only safety
v1 exposes no write, execution, or generic-access tool; every registered tool is R0 (low risk) and L0 (public data).
3
Data integrity
Canonical records change only through the versioned ingestion pipeline's content-hash diff and record-version history, never by ad hoc edit.
4
Auditability
Every tool call produces a structured audit event with a trace id, and every canonical record change is versioned.
5
Correctness
Tool selection and arguments are enforced by Zod schemas and checked against a golden evaluation set.
6
Availability
The stateless design allows ordinary horizontal scaling; the service targets 99.9% monthly availability.
7
Performance
The target is under 1,000 ms p95 at the Worker boundary for a simple D1-backed tool.
8
Visual presentation
The web surface (Home, Docs, Playground, Learn) is a later addition on top of the protocol service, not the core design constraint.
The system would rather say it found no reliable source than guess at an answer.
Technical behavior: returns NO_AUTHORITATIVE_SOURCE.
Can AI modify data?
No. Every MCP tool is read-only.
Every registered tool must declare readOnly: true and
riskLevel: 'R0' before it can be called. Forbidden generic tools
(execute_sql, run_shell, fetch_url,
read_file, write_file) simply do not exist.
Can AI ask the system to run SQL or a shell command?
No. Those tools do not exist here.
Every tool argument is checked by a strict Zod schema with explicit length and range bounds before it reaches a repository, and every SQL statement uses bound parameters, never a string-built query.
What if a document contains a malicious instruction?
It is never executed as a command.
There is no LLM anywhere in the MCP request path, so text inside a document body is returned to the client verbatim, never executed as an instruction.
Can the database be reached directly from the internet?
No. Only through the Worker's own binding.
The R2 bucket has no public bucket domain or r2.dev access configured
in any environment; every read goes through the Worker's own binding.
Could a test environment accidentally touch production data?
No. Each environment is fully separate.
Dev, staging, and production each have their own D1 database and R2 bucket, so a mistake in one environment cannot touch another environment's data.
Is every query tracked?
Yes, with a structured audit trail.
Every tool call emits a structured audit event (trace id, tool name and version, an argument hash rather than raw arguments, result count, latency, status), captured by Cloudflare Workers Logs.
For engineers who want the implementation details behind the tech-stack cards above.
packages/policy's tool registry and authorize() check: every
tool must be registered with a name, version, risk level, read-only flag, and maximum
data class before it can run. All 11 v1 tools are registered R0 (public read-only) and
L0 (public data).
Zod schemas (packages/schemas) validate every tool's input and output,
and every row read back from D1 before it is trusted as domain data.
packages/domain: one service class per data family (for example
ServiceCatalogService). This is the only layer allowed to call a
repository directly: MCP tool handlers never touch D1 or R2 themselves.
packages/database: Cloudflare D1 (SQLite)-backed repository
implementations, one repository interface per entity, all bound-parameter SQL.
Cloudflare R2 holds immutable raw source snapshots, archived before any parsing
happens and deduplicated by content hash so an unchanged source is never re-archived.
A shared content-hash helper (sha256HexOfJson) is used by ingestion's
change-detection and is reflected in every record's provenance, so every result
carries its source, version, and verification time.
packages/audit's structured JSON audit events (one per tool call: trace
id, tool name and version, scope decision, an argument hash rather than raw arguments,
result count, latency, status), captured automatically by Cloudflare Workers Logs; no
separate logging service.
AI4X MCP is made of several deployment units kept strictly separate: the public MCP query path, the data-sync job, Ask AI4X, and the static website each have a clear boundary and never bypass one another's intended path.
/mcpai4x-ingestion-workerPOST /api/ask-ai4x@cf/google/gemma-4-26b-a4b-itSpecific paths must be handled by Worker code first, never served as a static file.
Currently includes:
/mcp
/health
/about
/api/ask-ai4x
The run_worker_first rule guarantees these paths are always routed to Worker code first.
Request
|
v
run_worker_first
|
+--> /mcp ------------> MCP handler
|
+--> /health ---------> Worker handler
|
+--> /about ----------> Worker handler
|
+--> /api/ask-ai4x ---> Ask AI4X handler
|
`--> everything else -> Static Assets
For system maintainers and engineers.
Three environments (dev, staging, production), each deployed independently with
wrangler deploy --env <name> and each bound to its own D1 database
and R2 bucket.
Production is reached at the custom domain
https://ai4x.mcp.ntpu.ai (a Cloudflare Custom Domain on the account-owned
ntpu.ai zone), plus its *.workers.dev fallback hostname. An
ai4x.ntpu.edu.tw subdomain was considered and not used for this service,
since that zone belongs to the university's own site and is not under this project's
Cloudflare account.
Release order: lint, typecheck, unit tests, contract tests, integration tests, security tests, evaluation smoke tests, staging deploy, staging smoke probes, manual production approval, production deploy, post-deploy smoke tests.
Rollback: wrangler rollback <version-id> --env <env>
reactivates a previously uploaded Worker Version in seconds. This does not roll back
D1 or R2 state; a data-level mistake is fixed with a forward migration instead.
Every D1 schema change is a versioned migration file, applied with
wrangler d1 migrations apply. The schema is never mutated at Worker
startup.