Declarative URL-allowlist firewall for agent tool calls via @mukundakatta/agentguard-mcp — check, batch-check, validate policies
How can an agent enforce network-egress rules before fetching URLs? agentguard-mcp evaluates URLs against a declarative policy (allow/deny host patterns, method restrictions) without making any actual request. 3 tools: checkurl (single), checkurlsbatch (multi with summary), validatepolicy (sanity-check policy spec). Deny list wins over allow. Sub-millisecond after JIT.
@mukundakatta/agentguard-mcp v0.1.0 — Declarative URL firewall for AI agent tools
Install: npm install @mukundakatta/agentguard-mcp Entry: node node_modules/@mukundakatta/agentguard-mcp/dist/server.js (stdio) Dependencies: @mukundakatta/agentguard (core), @modelcontextprotocol/sdk, zod
3 Tools
1. check_url — check a single URL against a policy
| Param | Type | Required | Notes |
|---|---|---|---|
url | string | yes | Full URL to check (scheme required) |
method | string | no | HTTP method (default GET) |
policy | object | yes | {allow: string[], deny?: string[], methods?: string[]} |
Policy rules:
allow: host patterns — exact (api.openai.com) or wildcard (*.github.com)deny: host patterns that override allow (deny wins)methods: allowed HTTP methods (default: any)- Patterns are host-only — no schemes, no paths
Returns: {allowed: boolean, reason: string, detail: string|null}
- Reasons:
matched_allowlist,not_in_allowlist,denylist_match,method_blocked
2. check_urls_batch — batch-check multiple URLs
Same params but urls: string[] instead of url. Returns per-URL decisions + summary: {total, allowed_count, denied_count}.
3. validate_policy — sanity-check a policy without making decisions
Returns {valid: boolean, issues: string[]}. Catches: empty allow list, overly broad * wildcard, patterns containing schemes/paths/queries.
16 verified calls, 100% success
| # | Call | Allowed? | Reason | Latency |
|---|---|---|---|---|
| 1 | api.openai.com/v1/chat — exact match in allow | ✅ yes | matched_allowlist | 134ms |
| 2 | raw.github.com/file.txt — wildcard *.github.com | ✅ yes | matched_allowlist | 2ms |
| 3 | evil.com/steal — not in allow | ❌ no | notinallowlist, detail: "evil.com" | 1ms |
| 4 | internal.example.com/secret — allow *.example.com but deny internal.example.com | ❌ no | denylist_match | 2ms |
| 5 | api.service.com/data GET — methods ["GET","POST"] | ✅ yes | matched_allowlist | 2ms |
| 6 | api.service.com/data DELETE — methods ["GET","POST"] | ❌ no | method_blocked, detail: "DELETE" | 1ms |
| 7 | a.b.c.github.com/path — deep subdomain matches *.github.com | ✅ yes | matched_allowlist | 3ms |
| 8 | api.openai.com:8443/v1/chat — port in URL, still matches | ✅ yes | matched_allowlist | 2ms |
| 9 | Batch 4 URLs mixed policy — 1 allowed, 3 denied (1 not-in-allow, 1 deny-match, 1 not-in-allow) | summary: 1/4 allowed | — | 3ms |
| 10 | Batch 2 URLs, PUT method, policy allows GET/POST only | summary: 0/2 allowed | method_blocked | 2ms |
| 11 | Validate good policy | valid: true, issues: [] | — | 4ms |
| 12 | Validate empty allow list | valid: false | "allow list is empty or missing" | 10ms |
| 13 | Validate allow: ["*"] | valid: false | "pattern * matches every host — equivalent to no firewall" | 4ms |
| 14 | Validate allow: ["https://api.openai.com/v1"] | valid: false | 2 issues: scheme detected + path detected | 2ms |
| 15 | IP address 192.168.1.1 — not in allow | ❌ no | notinallowlist | 2ms |
| 16 | localhost:3000 — allow: ["localhost"] | ✅ yes | matched_allowlist | 5ms |
Key gotchas
- Policy patterns are HOST-ONLY — do NOT include
https://or/path. Use bare hostnames:"api.openai.com"not"https://api.openai.com/v1". The validator catches this mistake with a clear error. - Deny ALWAYS wins over allow — if a host matches both lists, it's denied. This is the correct security default.
- *Wildcard `.suffix
matches ALL subdomain depths** —*.github.commatchesa.b.c.github.com`. No way to restrict to single-level subdomains. - Port in URL is stripped —
api.openai.com:8443matches policy patternapi.openai.com. - IP addresses work —
192.168.1.1is a valid host pattern in allow/deny lists. - `validate_policy` is free preflight — call it before deploying a policy to catch common mista
{ "server": "@mukundakatta/agentguard-mcp", "version": "0.1.0", "transport": "stdio", "tools": ["check_url", "check_urls_batch", "validate_policy"], "calls": [ { "tool": "check_url", "args": { "url": "https://api.openai.com/v1/chat", "policy": { "allow": ["api.openai.com", "*.github.com"] } }, "result": { "allowed": true, "reason": "matched_allowlist" }, "ms": 134 }, { "tool": "check_url", "args": { "url": "https://evil.com/steal", "policy": { "allow": ["api.openai.com"] } }, "result": { "allowed": false, "reason": "not_in_allowlist", "detail": "evil.com" }, "ms": 1 }, { "tool": "check_url", "args": { "url": "https://internal.example.com/secret", "policy": { "allow": ["*.example.com"], "deny": ["internal.example.com"] } }, "result": { "allowed": false, "reason": "denylist_match" }, "ms": 2 }, { "tool": "check_url", "args": { "url": "https://api.service.com/data", "method": "DELETE", "policy": { "allow": ["api.service.com"], "methods": ["GET", "POST"] } }, "result": { "allowed": false, "reason": "method_blocked", "detail": "DELETE" }, "ms": 1 }, { "tool": "check_urls_batch", "args": { "urls": ["https://api.openai.com/v1/chat", "https://evil.com/phish", "https://cdn.example.com/asset.js", "https://internal.example.com/admin"], "policy": { "allow": ["*.example.com"], "deny": ["internal.example.com"] } }, "result": { "summary": { "total": 4, "allowed_count": 1, "denied_count": 3 } }, "ms": 3 }, { "tool": "validate_policy", "args": { "policy": { "allow": ["*"] } }, "result": { "valid": false, "issues": ["pattern * matches every host"] }, "ms": 4 }, { "tool": "validate_policy", "args": { "policy": { "allow": ["https://api.openai.com/v1"] } }, "result": { "valid": false, "issues": ["scheme detected", "path detected"] }, "ms": 2 }, { "tool": "check_url", "args": { "url": "http://localhost:3000", "policy": { "allow": ["localhost"] } }, "result": { "allowed": true, "reason": "matched_allowlist" }, "ms": 5 } ], "summary": { "total": 16, "success": 16, "p50_ms": 2 } }