Lab Guide: Unit 2 — Agent Tool Architecture
CSEC 601 | Weeks 5–8 | Semester 1
Four labs covering MCP server implementation, tool design patterns, structured outputs, and RAG-based security knowledge systems.
Your Context Library — A Running Artifact
Throughout Unit 2, you will build four MCP tools across four weeks. After each week's lab, you will capture what worked — design decisions, schema patterns, failure modes, and reusable prompts — into a file called unit2/context-library.md. This is your context library: a living document that grows as your understanding grows.
At the start of each week, read the previous entry before opening any code. At the end of each week, write the new entry before closing your editor. By Week 8, your context library is a complete record of how you built an AI system from scratch — and a reusable asset for every AI project you work on after this course.
The context library format is your own. Start with: what I built, what I learned, what I would do differently, what I want to remember. Evolve the format as you find what's useful.
Week 5 — Model Context Protocol (MCP): Your First Server
Lab Goal: Build a functioning MCP server in Python that exposes a query_cve tool to Claude Code. By the end, Claude should be able to fetch live CVE data from the NVD API using natural language in your terminal.
Knowledge Check — Week 5
Complete before starting the lab.
1. What are the three architectural components of the Model Context Protocol?
2. Which MCP transport is used for local Claude Code integrations?
3. What does MCP's tool discovery mechanism expose to an agent?
Lab Exercise: CVE Lookup MCP Server
Architecture note: You are building an MCP server — a Python process that Claude Code connects to. Claude becomes the client. The server wraps the NVD API and exposes it as the query_cve tool. Claude never calls the NVD API directly.
# macOS / Linux
mkdir -p ~/noctua-labs/unit2/week5/cve-mcp
cd ~/noctua-labs/unit2/week5/cve-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install mcp httpx pydantic
# Windows PowerShell
mkdir $HOME\noctua-labs\unit2\week5\cve-mcp -Force
cd $HOME\noctua-labs\unit2\week5\cve-mcp
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install mcp httpx pydanticGet an NVD API key before Step 6. Without one, the NVD API rate-limits at 5 requests per 30 seconds — easy to hit during testing. Registration is free and instant: nvd.nist.gov/developers/request-an-api-key. Store the key as an environment variable: export NVD_API_KEY=your-key-here (macOS/Linux) or $env:NVD_API_KEY="your-key-here" (PowerShell), or place it in a .env file. Your server reads it with os.environ.get("NVD_API_KEY") and passes it as the apiKey query parameter.
# schema.py — starter (create in ~/noctua-labs/unit2/week5/cve-mcp/schema.py)
import re
from typing import Optional
# Input schema
CVE_PATTERN = re.compile(r"^CVE-\d{4}-\d{4,}$", re.IGNORECASE)
# Why: regex validates format before any network call — rejects garbage early,
# prevents malformed strings from reaching the NVD API
class QueryCVEInput:
cve_id: str # Required — must match CVE_PATTERN
include_remediation: Optional[bool] = False
# TODO: add the regex constraint as an annotation or validator
# TODO: document why include_remediation defaults to False
# (hint: remediation text can be long — don't inflate context unless needed)
# Output schema
class QueryCVEOutput:
id: str # Normalized CVE ID, uppercase
description: str # English-language description from NVD
severity: str # CRITICAL / HIGH / MEDIUM / LOW / UNKNOWN
cvss_score: Optional[float] # Base score 0.0–10.0; None if not published yet
affected_products: list # TODO: what type should list items be?
remediation: Optional[str] # Only populated when include_remediation=True
error: bool = False # True if API call failed — never raise, always return
# TODO: add an error_code field (str) and error_message field (str)
# Why: callers check error=True rather than catching exceptions —
# MCP tools should return structured errors, not raiseOptional extension — build a local mock API. Instead of calling the live NVD API, you can build a local Flask or FastAPI server that returns mock CVE data. This demonstrates the service layer pattern more clearly (your MCP server doesn't know or care whether it's talking to the real NVD or a local mock) and works offline. Instructions: create a simple server returning hardcoded CVE JSON at the same endpoint your MCP tool expects. Swap the base URL in your MCP config.
(Theory: Week 5 Day 1 — Service Layer Pattern)
Extension: Generate domain-specific mock data. The mock data in this lab uses generic patterns. For your actual project, use Claude to generate mock data that matches your real domain: realistic CVE IDs, plausible CVSS scores, edge-case severity values, and intentional anomalies to test your validation logic. Prompt: "Generate 20 rows of mock CVE data including 3 edge cases that would fail naive validation — for example, a CVE with a missing CVSS score, a CVE with an unusually long description, and a malformed severity field." This practice — generating realistic test data with AI — is one of the highest-ROI uses of LLMs in a development workflow. Your validation logic is only as good as the edge cases it's been tested against.
# server.py — stub version (your starting point)
# Create this file in ~/noctua-labs/unit2/week5/cve-mcp/server.py
import re
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
app = Server("cve-lookup")
CVE_PATTERN = re.compile(r"^CVE-\d{4}-\d{4,}$", re.IGNORECASE)
@app.tool()
async def query_cve(cve_id: str) -> dict:
"""
Look up a CVE by ID and return structured vulnerability data.
Args:
cve_id: A valid CVE identifier, e.g. "CVE-2021-44228"
Returns:
dict with keys: id, description, severity, cvss_score,
affected_products (list), remediation (str)
Raises:
ValueError: if cve_id does not match the CVE format pattern
RuntimeError: if the NVD API is unreachable after retries
TODO: Implement this function.
Hints:
- Validate cve_id against CVE_PATTERN before making any network call
- NVD API endpoint: https://services.nvd.nist.gov/rest/json/cves/2.0
Query param: ?cveId={cve_id}
- Parse response: vulnerabilities[0].cve.descriptions, metrics, weaknesses
- On HTTP 429 (rate limit): return a structured error dict, do NOT raise
- On any other network failure: return a fallback dict with error=True
"""
raise NotImplementedError("Your implementation goes here")
async def main():
async with stdio_server() as streams:
await app.run(*streams, app.create_initialization_options())
if __name__ == "__main__":
import asyncio
asyncio.run(main())Show solution
# server.py — reference implementation
import re, httpx, asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
app = Server("cve-lookup")
CVE_PATTERN = re.compile(r"^CVE-\d{4}-\d{4,}$", re.IGNORECASE)
NVD_BASE = "https://services.nvd.nist.gov/rest/json/cves/2.0"
@app.tool()
async def query_cve(cve_id: str) -> dict:
if not CVE_PATTERN.match(cve_id):
raise ValueError(f"Invalid CVE format: {cve_id!r}. Expected CVE-YYYY-NNNNN.")
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(NVD_BASE, params={"cveId": cve_id})
if resp.status_code == 429:
return {"error": True, "code": "rate_limited",
"message": "NVD API rate limit hit. Retry after 30s."}
resp.raise_for_status()
data = resp.json()
vuln = data["vulnerabilities"][0]["cve"]
desc = next((d["value"] for d in vuln["descriptions"] if d["lang"] == "en"), "")
metrics = vuln.get("metrics", {})
cvss = None
severity = "UNKNOWN"
for key in ("cvssMetricV31", "cvssMetricV30", "cvssMetricV2"):
if key in metrics:
m = metrics[key][0]["cvssData"]
cvss = m.get("baseScore")
severity = m.get("baseSeverity", "UNKNOWN")
break
return {"id": cve_id, "description": desc, "severity": severity,
"cvss_score": cvss, "affected_products": [],
"remediation": "See NVD for vendor advisories."}
except Exception as e:
return {"error": True, "code": "api_error", "message": str(e)}
async def main():
async with stdio_server() as streams:
await app.run(*streams, app.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())claude
# Paste prompt: "Complete the TODO in this server.py stub.
# Follow the docstring exactly. Do not change the function signature.
# [paste your stub file contents]"# Quick manual test (run in your venv)
python3 -c "
import asyncio
from server import query_cve
# Test 1: invalid format — should raise ValueError
try:
asyncio.run(query_cve('CVE-ABC'))
print('FAIL: should have raised ValueError')
except ValueError as e:
print(f'PASS: invalid format rejected — {e}')
# Test 2: valid format, live API (if connected)
result = asyncio.run(query_cve('CVE-2021-44228'))
if 'error' not in result:
print(f'PASS: returned severity={result[\"severity\"]}')
else:
print(f'INFO: API unreachable — fallback returned correctly: {result}')
"⚠ Before registering tools: apply the Principle of Least Privilege. Every tool you expose increases your MCP server's attack surface. Before adding a tool, ask: Does this agent actually need this capability? What's the minimum scope needed? What should this tool never be able to do? Overpermissioned MCP servers enable capability amplification — an agent that should only read CVE data can scan networks if those tools exist. Define scope boundaries before you write the implementation.
(Theory: Week 5 Day 1 — Principle of Least Privilege)
# Register with Claude Code (user scope = available in all sessions)
claude mcp add --scope user cve-lookup \
~/noctua-labs/unit2/week5/cve-mcp/.venv/bin/python \
~/noctua-labs/unit2/week5/cve-mcp/server.py
# Confirm registration
claude mcp list
# Expected output: cve-lookup ~/noctua-labs/unit2/week5/cve-mcp/server.pyCommon failure: wrong Python path. If you point claude mcp add at python3 (system Python), the server will fail with ModuleNotFoundError: No module named 'mcp' because the mcp package is in your venv, not the system. Use the full path to .venv/bin/python inside your project directory. Verify: ~/noctua-labs/unit2/week5/cve-mcp/.venv/bin/python -c "import mcp; print('ok')"
/check-antipatterns ~/noctua-labs/unit2/week5/cve-mcp/
# Fix all CRITICAL findings before submitting
# Target: CONDITIONAL or READY statusThe audit produces findings at four severity levels. Here is what two common findings look like on an MCP server — and why they matter:
CRITICAL — Silent error swallowing [server.py:47]
except Exception:
pass
Why it matters: your tool returns {} with no error flag. Claude sees
an empty result and reasons from it as if it were valid data. The
caller has no way to know the NVD API call failed. On a governance
platform this is worse than a crash — the system appears to work.HIGH — New connection per request [server.py:31]
async with httpx.AsyncClient() as client:
resp = await client.get(...)
Why it matters: works correctly in dev and passes all tests. Under
concurrent load (10+ simultaneous CVE lookups) this exhausts
available connections. Fix: create the client once at module level
and reuse it across requests.The pattern: CRITICAL findings are silent — they produce wrong behavior with no signal. HIGH findings are load-dependent — they work in dev and fail in production. Neither will fail a test. Both require the deliberate audit lens to catch.
You just built your first reusable MCP tool. This is exactly what course skills are designed for. A skill is a custom /command you create once and reuse in any project. Create a /register-cve-server skill so any future session can spin up and connect your MCP server in one command — no re-explaining, no copy-pasting setup steps.
Custom slash commands live in ~/.claude/commands/ (user-global) or your project's .claude/skills/ directory. Use this prompt:
Then /register-cve-server becomes a one-liner for every future session.
Week 5 Deliverables
- server.py — working MCP server with query_cve tool, input validation, and error handling
- schema.py — documented input/output schemas with security justifications for each constraint
- test-results.md — results from natural language queries, edge case tests, and tool discovery screenshot
- MCP Analysis Report (500 words) — how MCP solves the integration sprawl problem; what security advantages does tool discovery + schema validation provide?
Update your context library. Before moving to the next week, add one entry to your context library from this week's lab: a prompt that worked well, a pattern you discovered, or a schema you designed. Build the habit now.
Week 6 — Tool Design Patterns for Security Agents
Lab Goal: Extend your CVE server to expose four security tools, implementing rate limiting, input sanitization, and the "Pit of Success" design principle — making the secure path the easy path.
Knowledge Check — Week 6
1. What does the 'Pit of Success' principle mean for MCP tool design?
2. Why should MCP servers validate inputs even when the calling agent is trusted?
3. What should every MCP tool invocation log for security compliance?
query_cve tool functional, (2) your Week 5 test suite passing (Step 3c checks pass), (3) the server confirmed running and reachable in Claude Code. If Week 5 was done in a previous session, restart your server and re-verify tool discovery before starting Week 6 steps.
query_cve tool does not respond, fix it before the Week 6 steps — Week 6 builds on a working Week 5 server, not a broken one.
# Verify Week 5 server is alive before starting Week 6
# macOS / Linux
cd ~/noctua-labs/unit2/week5/cve-mcp
source .venv/bin/activate
python3 -c "
import asyncio
from server import query_cve
result = asyncio.run(query_cve('CVE-2021-44228'))
print('Week 5 server OK:', 'error' not in result or result.get('code') == 'rate_limited')
"
# Windows PowerShell
cd $HOME\noctua-labs\unit2\week5\cve-mcp
.venv\Scripts\Activate.ps1
python -c "
import asyncio
from server import query_cve
result = asyncio.run(query_cve('CVE-2021-44228'))
print('Week 5 server OK:', 'error' not in result or result.get('code') == 'rate_limited')
"
# Expected: Week 5 server OK: True
# If False: fix your Week 5 server before proceedingLab Exercise: Extend to a Multi-Tool Security Server
# Claude Code prompt:
# "Add a query_asset_exposure tool to my existing MCP server that:
# 1. Accepts a CVE ID and optional asset_tag parameter
# 2. Cross-references against a mock asset inventory (hardcoded for this lab)
# 3. Returns: affected_assets (list), exposure_level (critical/high/medium/low), remediation_priority
# 4. Validates CVE ID format before processing
# 5. Logs every invocation with sanitized inputs"
claudeRate limiting and audit logging are architectural foundations, not optional additions. Build them before you wire in your tools — not after. When rate limiting is built first, every tool inherits it automatically. When audit logging is built first, every tool call is logged from day one. Retrofitting these controls after the fact is where gaps appear.
(Theory: Week 6 Day 1 — Defense-in-Depth for Tool Servers)
Rate limiting and audit logging are cross-cutting concerns — they belong in the server architecture before you implement any tools. A decorator pattern wraps every tool call at the dispatch layer. Steps 3 and 4 set up that foundation; the tools you build in Step 2 run on top of it automatically.
This is the Pit of Success principle: make the secure path the easy path. When security controls are foundational, every new tool inherits them without extra work.
Apply this to your server
Stop and answer these four questions for YOUR server before continuing. A threat model you don't finish is a threat model that doesn't protect you.
- What is the most valuable data your MCP server can access? Who (or what) would want unauthorized access to it?
- Which of your tools has the widest blast radius if compromised? What could an attacker do with it?
- If your MCP server were replaced by a malicious clone, what is the first thing an attacker would do with your tools?
- What is one control you don't currently have that would prevent the most serious attack scenario you identified?
/check-antipatterns ~/noctua-labs/unit2/week6/multi-tool-server/
# Target: CONDITIONAL or READY before submitting
# BLOCKED = fix CRITICAL findings first
# Include report output in your deliverables packageBefore specifying your tool, run /think with your tool selection problem as the argument. Let the CCT session surface what you actually know about the operational context before committing to a design. Then run /build-spec to capture the decisions that emerged. Build from the spec.
The spec is only as good as the thinking that preceded it. This workflow applies any time you make a significant design decision — tool design, context engineering, RAG architecture, governance decisions.
Week 6 Deliverables
- Week 6 deliverable: Extend your Week 5 tool. Add rate limiting (if not already in place), add one additional security-relevant tool of your choosing, and apply threat modeling to your multi-tool server using the GTG-1002 framework. Your deliverable is your extended MCP server plus a brief threat model document.
- server.py (updated) — extended multi-tool server with rate limiting, input validation, and structured audit logging
- Tool Design Document — for each tool: input schema, validation rules, error codes, and rate limit rationale
- threat-model.md — GTG-1002 threat model applied to your server
- Injection Test Report — results of your parameter injection attempt and how validation stopped it
Update your context library. Before moving to the next week, add one entry to your context library from this week's lab: a prompt that worked well, a pattern you discovered, or a schema you designed. Build the habit now.
Operation Forge Fire — Separate Lab Session
Estimated time: 60–90 minutes
Prerequisites: Working MCP server from Weeks 4–5 with at least 2 functional tools, Week 6 multi-tool extension complete
Deliverable: Completed Forge Fire report saved as
unit2/forge-fire-report.mdSession type: Timed exercise — complete this in one sitting
Prerequisites for Operation Forge Fire:
- Network recon basics: Port scanning identifies which ports on a target are open and what services are listening. This is legal only against systems you own or have explicit written permission to test.
- Red team / blue team structure: In this exercise, the red team (attacker) uses your MCP tools to simulate reconnaissance; the blue team (defender) monitors for anomalous tool call patterns. You play both roles.
- Authorization is required: The GTG-1002 attacker used a false persona to imply authorization. Your authorization is real and documented (your testing policy, below). The difference is not the tools — it's the authorization context.
- CCT in code: Your blue team monitor should embed Pillar 2 questions in its output ("What would the network team say about this pattern? Is this a legitimate service health check?")
Required: Complete your security testing policy before running any red team tools.
Create responsible-testing-policy.md in your project directory with at minimum:
- Scope: What systems are in scope for this test (localhost only for this lab)
- Authorization: Who authorized this test and how it is documented
- Tools in use: What tools will be run and what they do
- Data handling: How findings will be stored and who can access them
- Transparency test: Would you be comfortable submitting your complete AI conversation transcript to your CISO alongside your findings report? If not, don't run the test.
This is not bureaucracy. In professional security work, authorization documentation is what separates penetration testing from unauthorized access. The GTG-1002 attacker lacked it. You have it. Document it.
Forge Fire Lab Steps
# Pre-flight: verify Docker is installed and running
docker --version
docker ps
# Expected: version output + empty container list (no error)
# If you get "Cannot connect to the Docker daemon" — start Docker Desktop first
# If you get "command not found" — install Docker before proceeding (see lab-setup.html)
# Clean up any leftover containers from a previous attempt
docker rm -f forge-nginx forge-postgres 2>/dev/null; true
# Launch target containers — authorized localhost lab environment only
docker run -d -p 8080:80 --name forge-nginx nginx:alpine
docker run -d -p 5432:5432 --name forge-postgres \
-e POSTGRES_PASSWORD=labpassword postgres:15-alpine
python3 -m http.server 9000 &
# Verify all three targets are reachable before proceeding
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080 && echo " nginx OK"
python3 -c "import socket; s=socket.create_connection(('localhost',5432),2); print('postgres OK'); s.close()"
curl -s -o /dev/null -w "%{http_code}" http://localhost:9000 && echo " python-http OK"
# Expected: 200 nginx OK / postgres OK / 200 python-http OK
# Do NOT proceed to FF-2 until all three show OK# Claude Code prompt:
# "Add port_scan and banner_grab tools to my MCP server. Requirements:
# 1. ALLOWED_TARGETS = frozenset({'localhost', '127.0.0.1'}) — hard-coded, not configurable
# 2. security_testing_authorized parameter required (boolean) — schema validates presence
# 3. port_scan: scan list of ports, return open/closed status per port
# 4. banner_grab: connect to open port, read first 1024 bytes of response
# 5. Both tools log every call to audit log with target, ports, timestamp"
claude# Claude Code prompt (red team framing — authorized, transparent):
# "I am running an authorized red team lab exercise on localhost Docker containers.
# Testing policy: responsible-testing-policy.md (scope: localhost only).
# security_testing_authorized=true.
# Step 1: Use port_scan to scan localhost ports 80, 443, 5432, 8080, 9000, 3306, 6379, 22.
# Step 2: Use banner_grab on any open ports to identify services.
# Step 3: Use query_cve to look up CVEs for identified service versions.
# Report what an attacker would learn from this reconnaissance."What Forge Fire reveals: individual tools cannot detect multi-tool attack chains. Each tool (port_scan, banner_grab, query_cve, query_asset_exposure) sees only its own invocations. The attack pattern — port_scan → banner_grab → query_cve → query_asset_exposure — is only visible when you analyze tool call sequences across tools. This is what behavioral monitoring requires, and why a blue team that monitors individual tool calls will miss coordinated attacks. Keep this in mind for Semester 2 Unit 5, where multi-agent architectures address this gap.
# Claude Code prompt:
# "Add blue team monitoring tools to my MCP server:
# 1. detect_port_scan(time_window_seconds): reads audit log, finds source IPs
# with >4 unique destination ports in the window, returns alert with severity
# 2. analyze_reconnaissance_pattern(source_ip, time_window_seconds): applies CCT
# framework — embed Pillar 2 output: 'What would the network team say about
# this pattern? Is this a legitimate service health check?'
# Both tools read from the JSON audit log, not live network data."
claudeLab vs. production: In this lab, the blue team monitor receives log data passed directly from the red team exercise. In a production environment, the equivalent of log_connection() would be called by a network tap, SIEM webhook, eBPF probe, or security data pipeline — not manually. The detection logic you're writing here is the same; only the data ingestion layer changes.
Forge Fire Deliverables
- responsible-testing-policy.md — required before running any Forge Fire tools
- forge-fire-report.md — red team findings, blue team detection results, gap analysis
- forge-fire-debrief.md — your retrospective answers (see debrief below)
- Updated server.py — with port_scan, banner_grab, detect_port_scan, analyze_reconnaissance_pattern tools
- What was the hardest part of the exercise? Was it technical, time-based, or conceptual?
- Which tool in your server had the most unexpected behavior under pressure?
- If you had 30 more minutes, what would you have fixed first?
- What does your Forge Fire experience tell you about your system's production readiness?
unit2/forge-fire-debrief.md. This is not graded — it is for your own retrospective.
Cedar is Amazon's authorization policy language. It reads like English and enforces like a database constraint.
// Allow the analyst agent to use the query tool
permit (
principal == Agent::"analyst-agent",
action == Action::"query",
resource == Tool::"cve-lookup"
);
// Allow the reporter agent to read CVE data, but not write or delete
permit (
principal == Agent::"reporter-agent",
action in [Action::"read", Action::"list"],
resource in Resource::"cve-database"
);
// No agent can access the production database directly — ever
// Note: forbid cannot be overridden by any permit
forbid (
principal,
action,
resource == Database::"production-db"
);
Cedar's forbid is unconditional — no permit can override it. This is by design: your most critical restrictions (no direct database access, no PII export) go in forbid blocks so they can never be accidentally granted. Use permit for what agents CAN do; use forbid for what they MUST NEVER do.
What: Cedar is a policy language that defines what agents are allowed to do — which tools they can call, which resources they can access, under what conditions.
Why: Hard-coding permissions in agent code creates security debt. Cedar externalizes authorization so you can audit, rotate, and restrict permissions without touching agent code.
How to start: Define one Cedar policy per agent. Start with a default-deny posture (no permits) and add explicit permits for every capability the agent needs. If a capability isn't listed, it doesn't exist.
Week 7 — Structured Outputs & Security Reporting
Lab Goal: Build an MCP tool that generates structured security incident reports as both JSON (machine-readable) and Markdown (human-readable). Reports must be valid against a defined JSON Schema and parseable by downstream systems.
When a tool calls an AI model, you now have two latency budgets, two token costs, and two failure modes to manage. The outer agent's context window may include the inner model's full response. Design tool-calling chains the same way you design network calls: timeouts, retries, fallbacks, and circuit breakers. A tool that calls Claude internally and hangs for 30 seconds will stall your outer agent's entire reasoning loop. A tool that returns a 10,000-token response will silently inflate your outer agent's context on every call. Both problems are invisible until they're not.
The schema you defined in Week 4 is enforced here. Every tool result that enters the agent's context is validated against that schema before the model sees it. If you skipped schema validation in Week 4, you will feel it now — malformed tool results are the #1 source of agent hallucination in production. Unvalidated tool output is just unstructured text wearing a JSON costume. The model pattern-matches against it, fills gaps with plausible-sounding values, and returns an answer that looks correct but isn't. Validate at the tool boundary. Every time.
Knowledge Check — Week 7
1. Why is structured JSON output preferred over free-text in security automation?
2. What does JSON Schema validation catch that format checking alone cannot?
Lab Exercise: Security Report Generator
This step requires the Anthropic Python SDK. The basic pattern:
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
system="Return only valid JSON. No explanation.",
messages=[{"role": "user", "content": prompt}]
)
text = response.content[0].textFor enforcing structured output: a system prompt instruction ("Return only valid JSON") is simpler at this stage than tool_use. Handle anthropic.APIConnectionError, AuthenticationError, and RateLimitError explicitly — don't let them surface as unhandled exceptions.
Pattern: when should a tool call Claude? Use this pattern when the tool needs to classify, extract, or generate structured content from unstructured input — threat classification, CVE severity reasoning, report drafting. Keep the Claude call focused: pass only the data this specific task needs, not the full conversation history. Connect to your Week 5–6 schemas — if you defined output fields for your CVE or asset tools, reuse those field names here rather than redefining. (Theory: Week 7 Day 1 — Claude Calling Claude)
# Claude Code prompt:
# "Add a generate_incident_report tool to my MCP server that:
# 1. Accepts raw incident data as a string
# 2. Uses Claude API to structure it into the JSON Schema in report-schema.json
# 3. Validates the output with jsonschema library before returning
# 4. Returns both JSON and markdown versions
# 5. Raises ValidationError with details if schema validation fails"You've just implemented "Claude Calling Claude." One Claude session (your agent) called a tool that internally called Claude again (the report generator). This is composition: each Claude call has a focused, narrow task. The orchestrating Claude handles conversation; the tool-level Claude handles a single reasoning task (classification, extraction, generation). This pattern scales — you can chain multiple specialized Claude calls through your tool layer without the orchestrating Claude needing to know about them.
When an MCP tool calls the Anthropic API internally, you have created a composition layer. The outer agent (Claude Code) orchestrates and selects tools. The inner call does structured generation with a tight schema constraint. Each does what it's best at.
The outer agent is unaware the inner call is also Claude. The inner model can be chosen independently — Haiku is sufficient for template-constrained JSON generation and costs a fraction of Sonnet. The inner API key lives in the MCP server's environment, not accessible to the outer agent.
pip install jsonschema
python3 -c "
import json, jsonschema
schema = json.load(open('report-schema.json'))
report = json.load(open('generated-report.json'))
jsonschema.validate(report, schema)
print('Report is valid!')
"Week 7 Deliverables
- report-schema.json — complete JSON Schema for incident reports
- Updated server.py — with generate_incident_report tool and schema validation
- meridian-report.json and meridian-report.md — generated outputs from Week 1 incident
- Validation Test Results — screenshots/logs showing both passing and failing validation
Update your context library. Before moving to the next week, add one entry to your context library from this week's lab: a prompt that worked well, a pattern you discovered, or a schema you designed. Build the habit now.
Week 8 — RAG for Security Knowledge
Lab Goal: Build a Retrieval-Augmented Generation system that lets Claude query a curated security knowledge base (threat intel, runbooks, policy docs) without hallucinating answers from training data. This becomes your personal security AI assistant.
Knowledge Check — Week 8
1. What problem does RAG (Retrieval-Augmented Generation) solve for security knowledge systems?
2. Why are vector embeddings used for document retrieval in RAG systems?
Lab Exercise: Security Knowledge Base with ChromaDB + Claude
Starter corpus — download before Step 1. For this lab, use these three documents as your initial knowledge base: your threat-model.md from Week 6, the OWASP Agentic Top 10 summary, and the Meridian IR runbook from your context library. If you don't have these, ask Claude to help you generate brief placeholder versions. The corpus content matters less than the retrieval pipeline — focus on getting retrieval working correctly first. (Theory: Week 8 Day 1 — MCP vs. RAG Decision)
pip install chromadb anthropic sentence-transformers
mkdir -p ~/noctua-labs/unit2/week8/security-kb
cd ~/noctua-labs/unit2/week8/security-kbsentence-transformers downloads model weights on first use (~90MB–400MB depending on model). This happens at runtime, not install time. On a slow connection or in a restricted network environment, the first call will appear to hang. Add a progress indicator or test with a small model first (all-MiniLM-L6-v2 is the fastest to download). If your environment blocks outbound HTTP to Hugging Face, the download will fail silently and raise a cryptic error. Verify connectivity before this step: curl -I https://huggingface.co. The weights are cached after the first download — subsequent runs are instant.
# Claude Code prompt:
# "Build a document ingestion script that:
# 1. Reads .md files from kb-docs/ directory
# 2. Chunks each into 500-token segments with 50-token overlap
# 3. Generates embeddings with sentence-transformers all-MiniLM-L6-v2
# 4. Stores in ChromaDB with metadata: source_file, chunk_index, content
# Save the ChromaDB to ./chroma-db/ directory"
claudeTest your RAG failure modes before you trust your RAG success modes. Run three tests: (1) Query for something that does NOT exist in your index — does the agent hallucinate an answer or say it doesn't know? (2) Query with a typo or paraphrase — does retrieval still find the right chunks? (3) Query with an ambiguous question — does the agent ask for clarification or guess? A RAG system that doesn't fail gracefully is not production-ready. Document each test result in your RAG Failure Mode Report. The goal is not a RAG system that only works on expected queries — the goal is a system with predictable, safe behavior when inputs go outside expected bounds.
Vector store security check: Before trusting your ChromaDB prototype, answer four questions: (1) can you restrict retrieval by metadata such as source, owner, or classification? (2) do retrieved chunks include enough provenance for a human to judge trustworthiness? (3) can you remove or re-index stale documents cleanly? (4) if a poisoned chunk enters the corpus, do you have a way to identify and delete every derived entry? If the answer is "no" to any of these, the system is a useful prototype but not a secure retrieval service.
Week 8 Deliverables
- kb-docs/ — curated security knowledge base (minimum 5 documents)
- ingest.py + server.py (with search_security_kb tool) — fully working RAG pipeline
- RAG Evaluation Report — side-by-side comparison of RAG vs. no-RAG responses, with accuracy and grounding analysis
- RAG Failure Mode Report — documented results from Step 7 failure mode tests
- Unit 2 Final Summary (1 page) — how your MCP server evolved from a single CVE tool into a multi-tool, RAG-powered security assistant
Update your context library. Before moving to the next week, add one entry to your context library from this week's lab: a prompt that worked well, a pattern you discovered, or a schema you designed. Build the habit now.
You've built a 4-tool MCP server with RAG. That's not a homework assignment — that's a portfolio piece. Push your repository to GitHub public right now. Write a README that explains what it does, how to run it, and what security problems it solves. Security practitioners build reputations through open work. Someone else will use your CVE lookup tool. Someone will fork your RAG knowledge base. That's the point.
The security community grows stronger when practitioners share what works — including students and hobbyists. A working MCP security tool on GitHub is more valuable to the community than a private homework repo. Don't wait until it's "polished." Ship it.
Unit 2 Complete
You have built a production-capable MCP server with the following tools: query_cve, query_asset_exposure, generate_incident_report, and search_security_kb.