MCP Agent Toolkit
Provides web search capability via DuckDuckGo as a search provider, returning sanitized titles, URLs, and snippets for agent queries.
Integrates with Prometheus by exposing tool invocation counters and duration histograms through an optional HTTP metrics endpoint.
Provides read-only SQL query access to a SQLite database, supporting single SELECT/WITH statements with row limits and guardrails.
Click on "Deploy 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., "@MCP Agent ToolkitSearch the web for MCP security best practices and write a cited report to report.md"
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.
๐ MCP Agent Toolkit
A production-shaped Model Context Protocol server + autonomous agent client โ an LLM discovers four guarded tools over the open standard at runtime, recovers from live tool errors, and produces a cited report, with every invocation logged, rate-limited, and correlated by request ID.

Real run in the web console โ live OpenAI tool-calling loop over MCP
stdio: the agent queries the seeded DB, searches the web, writes a report
into the sandbox, and every step streams to the UI with labeled tool
cards. Prefer the terminal? The same loop runs in the CLI โ
docs/demo.gif +
docs/demo-transcript.txt, recorded via
npm run demo:record (asciinema cast in
docs/demo.cast).
๐ What this is
A standalone MCP server + agent client in strict TypeScript. The server
exposes four tools over the official @modelcontextprotocol/sdk
with stdio transport: web_search, db_query (read-only SQLite),
read_file and write_file (sandboxed). The client spawns the server as a
child process and speaks plain MCP โ tools/list, tools/call,
params._meta correlation โ exactly like Claude Desktop or any other
MCP-capable host would. Nothing is imported in-process; the protocol is the
boundary.
On top of the wire sits an autonomous agent loop: an OpenAI tool-calling
loop (with a deterministic StubLLM fallback for zero-secret operation)
that plans, calls tools, handles malformed responses and timeouts,
recovers from tool errors, and streams every step as structured events.
The point is MCP done right: guardrails and observability at the protocol boundary, not bolted onto a demo. Every tool call is schema-validated, rate-limited, sanitized on the way back (the web is untrusted โ OWASP LLM01), logged as JSON with a correlated request ID, and counted in Prometheus metrics.
Related MCP server: MCP Tool Server
๐ก Why this matters โ for your project, or for a technical reviewer
๐ The protocol is the real boundary. The agent discovers tools via
tools/listand invokes them viatools/callover JSON-RPC stdio โ no shared imports, no framework glue. Swap the client for any MCP host and the server just works.๐ก๏ธ Defense in depth, not a single regex.
db_queryis gated by a single-SELECTstatement guard and areadonlySQLite connection;read_file/write_filepass a lexical check and arealpathcontainment check that defeats symlink escapes. Each layer fails closed on its own.๐งผ Untrusted output never becomes instructions. Every tool result is sanitized server-side (control/bidi/zero-width chars stripped, injection patterns neutralized,
MAX_TOOL_OUTPUT_CHARScap) before it reaches the model context โ the OWASP LLM01 control for indirect prompt injection.๐ The server never sees the LLM key. The client forwards an explicit env allowlist to the spawned server process;
OPENAI_API_KEYcannot cross that boundary. Empty secrets count as absent, so CI runs with zero credentials.๐งพ Request-ID correlation end-to-end. The client stamps a UUID into
params._meta.requestIdper call; the server logs it on every invocation line โ client event stream joins server logs trivially.๐ฆ Every failure is a first-class signal. Timeouts, malformed responses, unknown tools and guardrail rejections normalize to typed error envelopes โ the agent sees a clean
failurereason, not a stack trace (the demo transcript shows a live SQL error recovered).๐งช The transport is real in tests too. The integration suite spawns the actual server over stdio and asserts request-ID correlation and rate-limit enforcement across the wire โ not a mock of MCP.
๐ CI green with zero secrets. Fork, clone,
npm ci && npm testโ 110 tests pass offline; the deterministicStubLLMandSEARCH_PROVIDER=noneneed no keys.
๐๏ธ Architecture
graph LR
subgraph client["Agent client"]
CLI[CLI entry] --> LOOP[Agent loop<br/>OpenAI tool calling]
BROWSER[Browser<br/>React console] -->|HTTP + SSE| WEB[Express adapter]
WEB --> LOOP
LOOP --> MC[MCP client<br/>StdioClientTransport]
LOOP -.->|no OPENAI_API_KEY| STUB[StubLLM<br/>deterministic]
LOOP -->|chat.completions| OAI[(OpenAI API)]
end
MC ==>|"JSON-RPC over stdio<br/>tools/list ยท tools/call<br/>requestId via params._meta"| MS
subgraph server["MCP server (child process)"]
MS[McpServer<br/>StdioServerTransport] --> REG[Tool registry<br/>+ guardrail wrapper]
REG --> RL[Rate limiter<br/>token bucket]
RL --> T1[web_search]
RL --> T2[db_query]
RL --> T3[read_file]
RL --> T4[write_file]
T1 --> SANE[Output sanitizer]
T2 --> SQLG[SELECT-only guard<br/>+ readonly conn]
T3 --> FS[Path sandbox]
T4 --> FS
REG --> LOG[pino JSON โ stderr]
REG --> MET[Prometheus metrics<br/>optional :METRICS_PORT]
end
T1 -->|HTTPS| EXT1[(Tavily /<br/>DuckDuckGo)]
T2 --> EXT2[(SQLite<br/>readonly)]
T3 --> EXT3[(data/sandbox/)]
T4 --> EXT3
style client fill:transparent,stroke:#8b949e,stroke-dasharray:5 5
style server fill:transparent,stroke:#8b949e,stroke-dasharray:5 5Full rationale and the tool/guardrail matrix:
docs/architecture.md.
๐ Evaluation-Driven Development (EDD)
The quality gates are binary and enforced in CI โ a regression fails the build rather than being shrugged off:
Dimension | Metric | Threshold | Latest run |
Test suite | unit + security + real-stdio integration | 100% green | 137/137 โ |
Coverage | lines / branches / functions / statements | โฅ 85% each | 98.2 / 86.8 / 100 / 97.5 โ |
Type safety |
| 0 errors | โ |
Lint |
| 0 warnings | โ |
Security suite | traversal ยท write SQL ยท injection ยท rate-limit rejections | all must fail loudly | โ |
UI E2E | Playwright โ 7 dataset questions + raw-field-name ban | 100% green | 9/9 โ |
Dependency audit |
| 0 high/critical | โ |
Code quality is necessary but not sufficient โ an agent can pass every
lint rule and still answer badly. So the product itself is evaluated with
a versioned dataset (evaluation/dataset.json,
v1.1.0 โ 14 questions across multi-tool, single-tool, error recovery,
adversarial and ambiguous categories) and an independent LLM-as-judge
(a separate prompt and call that sees the transcript, tool outputs and a
reference answer โ never the agent's own system prompt). Latest real run
(npm run eval, gpt-4o-mini, traced and published to LangSmith โ
scorecard committed at evaluation/scorecard.json):
Dimension | Threshold | Score |
Task success | โฅ 4.0 / 5 | 4.93 โ |
Grounding & citation quality | โฅ 4.0 / 5 | 4.43 โ |
Appropriate tool usage | โฅ 90% | 100% โ |
Adversarial prompt resistance | 100% | 3/3 โ |

Real LangSmith dataset (mcp-agent-toolkit-eval-v1.1.0): three published
experiments, each running all 14 questions end-to-end โ deterministic
checks (expected_tools, adversarial_resistance) at 1.00 alongside the
LLM-judge scores, plus P50/P99 latency per run.
The scorecard is honest by construction: the first live run failed
(grounding 3.71) and the fixes it motivated โ real DB schema in the
db_query description, schema-aware error recovery, judge access to tool
outputs and reference answers โ are what the current numbers reflect.
See evaluation/README.md for the rubric and
npm run eval:offline for the deterministic harness smoke test.
Reproduce: npm run test:cov (coverage gate), npm run test:e2e
(Playwright UI suite), npm run eval (live judge โ needs
OPENAI_API_KEY, publishes to LangSmith when LANGCHAIN_API_KEY is set).
๐ API
The server speaks standard MCP (JSON-RPC 2.0 over stdio): initialize,
tools/list, tools/call. Tools:
Tool | Input | Guardrails | Returns |
|
| provider allowlist, timeout, output sanitizer + size cap |
|
|
| statement guard + |
|
|
| lexical + |
|
|
| same containment, |
|
Example tools/call payload (real shape โ the client adds
_meta.requestId automatically):
{
"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {
"name": "db_query",
"arguments": { "query": "SELECT title, author FROM reports ORDER BY published_at DESC LIMIT 5" },
"_meta": { "requestId": "de30734e-8f77-4418-8a27-65557d8ade40" }
}
}Agent CLI:
npm run agent -- "question" # live OpenAI loop (needs OPENAI_API_KEY)
npm run agent -- "question" --stub # deterministic, zero secrets
npm run agent -- "question" --json # machine-readable event stream
npm run demo # seeded E2E run โ docs/demo-transcript.txt
npm run demo:record # record docs/demo.cast โ GIF via asciinema/agg๐ก Observability & cost
pino JSON logs on stderr (stdout is reserved for the MCP transport): every tool invocation logs tool name, sanitized input args, output preview, duration, ok/error,
session_idandrequest_id.Request-ID correlation: the client stamps
params._meta.requestId; the server echoes it on every log line โ join client events to server logs by UUID.Prometheus metrics (
@prometheus-io/client): invocation counters by tool+status and a duration histogram (p95-ready), exposed on an opt-inMETRICS_PORTHTTP endpoint so stdio stays clean.Cost: the only metered dependency is the LLM. With no
OPENAI_API_KEYthe whole system runs free onStubLLM; tool calls and the sandbox cost nothing.MAX_TOOL_ITERATIONSbounds worst-case token spend per run.
Real server log line from the recorded demo:
{"level":30,"service":"mcp-server","event":"tool_invocation","tool":"db_query",
"request_id":"de30734e-8f77-4418-8a27-65557d8ade40","session_id":"3bb996d6-โฆ",
"duration_ms":0,"ok":true,"input":"{\"query\":\"SELECT title, author FROM reports\"}",
"output":"[{\"type\":\"text\",\"text\":\"{\\\"status\\\": \\\"ok\\\"โฆ"}๐ Security posture
Sandboxed filesystem (lexical + realpath containment), read-only SQL
(statement guard and readonly connection), output sanitization
against indirect prompt injection (OWASP LLM01), per-tool/session rate
limiting, env allowlist so the server never sees OPENAI_API_KEY,
secret-redacted logs, gitleaks + npm audit in CI on pull_request
with contents: read. Full threat model and mechanism table:
SECURITY.md.
๐ Quick Start
npm ci
cp .env.example .env # optional โ paste OPENAI_API_KEY for the live loop
npm run seed:db # fixture DB for db_query (8 reports)
npm run web # web console โ http://localhost:3000
npm run demo # recorded E2E: seeds + runs, saves transcript
npm run agent -- "your question"The web console (npm run web) builds frontend/, serves it on
WEB_PORT and streams each run over SSE: typed tool cards, guardrail
outcomes and a markdown final answer โ local, single-user, no auth
(see SECURITY.md).
Degrades cleanly with zero secrets: no OPENAI_API_KEY โ deterministic
StubLLM; no SEARCH_API_KEY โ keyless DuckDuckGo; SEARCH_PROVIDER=none
โ fully offline. The entire CI suite runs with no credentials.
docker compose up # runs the default demo question once
docker compose run --rm agent "โฆ" # custom question
docker compose run --rm agent --stub "offline run, no keys"
docker compose up web # web console on http://localhost:3000The image is multi-stage (non-root node user), seeds the fixture DB at
build time, and mounts only data/sandbox/ as a volume โ the single
writable surface. .env is read at runtime via env_file; nothing is
baked in.
โ๏ธ Environment Variables
Variable | Required | Secret? | Purpose |
| no | โ | Live LLM for the agent loop; |
| no | โ | Model override (default |
| no | โ |
|
| no | โ |
|
| no | โ | Tavily key (optional paid provider) |
| no | โ | Override how the client spawns the server |
| no | โ | The only filesystem root tools may touch |
| no | โ | SQLite fixture path ( |
| no | โ | Per tool, per session (default 60) |
| no | โ | Agent loop bound โ must terminate |
| no | โ | Tool output truncation cap (untrusted data) |
| no | โ | FS size caps |
| no | โ | pino level; logs go to stderr |
| no | โ | Empty = disabled; e.g. |
| no | โ |
|
| no | โ | Web console port (default |
| no | โ | Allowed origin for |
| no | โ | LangSmith tracing + experiment publishing |
| no | โ |
|
| no | โ | LangSmith project (default |
Never commit secrets. See .env.example for the full,
commented template.
๐ Project Structure
src/
โโโ server/ # MCP server (stdio) โ separate process
โ โโโ server.ts # tool registration + guardrail wrapper + logging
โ โโโ tools/ # web_search, db_query, read_file, write_file
โ โโโ guardrails/ # path sandbox, SQL guard, rate limiter, sanitizer
โ โโโ observability/ # pino logger, Prometheus metrics, request ctx
โโโ client/ # agent CLI โ spawns the server, speaks MCP
โ โโโ mcp-client.ts # tools/list + tools/call + env allowlist
โ โโโ agent-loop.ts # tool-calling loop (bounded, must terminate)
โ โโโ events.ts # structured event stream (pretty|json)
โ โโโ llm/ # OpenAI impl + deterministic StubLLM
โโโ web/ # Express adapter โ /api/chat + SSE, run store,
โ # serves the built frontend
โโโ shared/ # config (env > .env, empty secrets = absent),
# ToolError + result envelopes
frontend/ # React + Vite + Tailwind console (own package.json)
evaluation/ # versioned dataset, LLM-as-judge, LangSmith runs,
# scorecard.json (real run) + README (rubric)
scripts/ # seed-db.ts, demo.ts, record-demo.ts, ui-qa.ts,
# make-demo-gif.ts
tests/ # 137 tests โ unit, security, real-stdio integration
tests/e2e/ # Playwright: 7 dataset questions + raw-field ban
docs/ # architecture.md, web-demo.gif, demo.gif + demo.cast,
# ui-qa/ screenshots
data/sandbox/ # the ONLY writable surface for the fs tools
.github/workflows/ # CI: lint โ typecheck โ tests+coverage โ build
# โ Playwright E2E โ audit + gitleaks๐งช Verification
npm run lint # eslint โ 0 warnings
npm run typecheck # tsc --noEmit strict โ 0 errors
npm run test:cov # 137 tests offline; โฅ85% all metrics (~98% lines)
npm run build # compile to dist/ (backend)
npm run test:e2e # Playwright UI suite โ offline, deterministic
npm run eval:offline # eval harness smoke test (StubLLM)
npm run eval # real dataset + LLM judge (needs OPENAI_API_KEY)
npm audit --omit=dev # 0 vulnerabilitiesCI (.github/workflows/ci.yml): lint โ typecheck โ tests+coverage โ build
โ Playwright E2E โ npm audit + gitleaks, on pull_request, Node
20/22/24 matrix, permissions: contents: read. A fork passes with zero
secrets โ the E2E job runs on StubLLM + SEARCH_PROVIDER=none.
๐ ๏ธ Stack
Node 20+ ยท TypeScript 5 strict ESM ยท @modelcontextprotocol/sdk (stdio
transport, tools/list/tools/call, _meta correlation) ยท OpenAI
tool-calling (chat.completions) ยท Express 5 + SSE (web adapter) ยท
React 19 + Vite + Tailwind CSS 4 + lucide-react (web console) ยท
better-sqlite3 (readonly fixture DB) ยท zod (input schemas) ยท pino
(JSON logs) ยท @prometheus-io/client (metrics) ยท Vitest +
@vitest/coverage-v8 ยท Playwright (UI E2E, deterministic offline) ยท
LangSmith (langsmith โ tracing, datasets, LLM-as-judge experiments) ยท
ESLint 10 + typescript-eslint ยท Docker multi-stage.
Known limitations & next steps
stdio only. The transport trusts the spawning client โ the correct MCP model for a local server. A remote deployment needs the HTTP transport plus authn/authz, deliberately out of scope.
In-process rate limiting. A horizontally scaled deployment would need a shared token-bucket store (e.g. Redis).
Keyless search quality. DuckDuckGo HTML scraping is the honest zero-cost fallback; a paid provider (Tavily) is a
SEARCH_PROVIDERflip away and returns richer results.Web console is local and single-user. No authentication or authorization โ one shared MCP process, one rate-limit budget. Safe for
localhost; exposing it beyond your machine means putting it behind an auth layer or reverse proxy first (seeSECURITY.md).Evaluation reflects the fixture world. The 14-question dataset exercises the seeded 8-row DB and sandboxed FS โ a strong signal for tool-use correctness on this surface, not a general capability claim. Judge scores carry LLM judge noise run-to-run; thresholds are averages, not per-question guarantees.
SQLite-centric SQL guard.
db_queryassumes SQLite semantics; porting to Postgres would add role-levelGRANT SELECTas a third enforcement layer.Token-level streaming. The SSE stream emits structured run/tool events; streaming partial assistant tokens to the UI is a natural next step.
๐ผ Need this for your own project?
I build production-shaped agentic AI systems โ MCP servers and clients, guardrail-first tool surfaces, cost-aware agent loops, and the observability discipline to actually trust them in production.
If you need an internal capability exposed safely to agents over an open protocol โ with sandboxing, injection defenses, audit-grade logging and a test suite that proves the guardrails fail loudly โ this codebase is the working template.
This same project can be adapted to your own systems: swap the four tools for your internal APIs, databases or file surfaces and the guardrails, agent loop and observability carry over unchanged.
๐ License
This server cannot be deployed
Maintenance
Related MCP Connectors
Find, vet, and run MCP tools through a secure audited gateway with prompt-injection risk scoring
Security & DLP proxy for MCP: tool-poisoning scans, PII redaction on tool args/results. Beta.
AgentGuard โ 20-tool AI safety MCP: policy preflight, risk scoring, audit logging, rate limits.
Enable secure connectivity between Sentry issues and debugging data, and LLM clients, using a Model Context Protocol (MCP) server.
Related MCP Servers
- AlicenseBqualityDmaintenanceA security-focused Model Context Protocol server that enables controlled local tool execution through strict network firewalls, filesystem protections, and rate-limiting policies. It features a plugin-based architecture for progressive tool discovery and includes reference implementations for web searching and bug tracking.15MIT
- FlicenseNot gradedqualityCmaintenanceA Model Context Protocol server that advertises tools with JSON schemas and executes tool calls safely, enabling AI agents to perform actions on real systems.-
- AlicenseNot gradedqualityCmaintenanceExposes database and external API tools via the Model Context Protocol, enabling LLMs to query SQLite and call public APIs with validated inputs and error handling.MIT
- FlicenseNot gradedqualityCmaintenanceEnables secure, stateful AI agents and LLM tools to access enterprise databases, knowledge bases, live system metrics, and sandboxed code execution through the Model Context Protocol, with authentication, rate limiting, and safety guards.-