Tools¶
All nine tools are registered in guard_core_mcp/server.py. validate_config, config_fields, search_docs and get_doc all accept a package argument that should be one of fastapi-guard, guard-core or guard-agent (the docs tools also know the bundled guard-core-ts corpus and the guard-core-app, guard-core-go, guard-core-php, guard-core-rs, guard-agent-go, guard-agent-ts, guard-agent-rs and guard-agent-php knowledge entries), but they don't validate it the same way. validate_config and config_fields reject an unrecognized value with {"error": "unknown package '<value>'; expected one of guard-core, fastapi-guard, guard-agent"}. search_docs has no such check, an unrecognized package just matches nothing, so it returns {"query": ..., "results": []}. get_doc returns {"error": "unknown doc path"} for either an unrecognized package or a path that doesn't exist under it, the two cases aren't distinguished in the response.
validate_config, config_fields and check_payload depend on the corresponding library being installed in the interpreter running the server. If it is not, they return a structured {"error": ..., "hint": ...} instead of raising, see Installation for exactly what that looks like and why. The three ecosystem tools (ecosystem, adapter_setup, wire_agent) are pure data served from the registry in guard_core_mcp/ecosystem.py and work in any environment, with or without the Guard libraries installed.
versions¶
No parameters. Reports which Guard libraries this server can introspect, at what version, and which library versions the bundled documentation and knowledge corpora cover. A null installed version means that library is absent from this interpreter, so any answer about it would be a guess rather than introspection.
Example call:
Example response:
{
"guard_core_mcp": "1.3.0",
"installed": {
"guard-core": "4.2.0",
"fastapi-guard": "8.0.2",
"guard-agent": "3.1.0"
},
"docs_bundled_for": {
"fastapi-guard": "8.0.2",
"guard-agent": "3.1.0",
"guard-core": "4.2.0",
"guard-core-ts": "4.2.0"
},
"knowledge_bundled_for": {
"guard-core-app": "2026.09.24",
"guard-core-go": "4.2.0",
"guard-core-php": "4.2.0",
"guard-core-rs": "4.2.0",
"guard-agent-go": "3.1.0",
"guard-agent-php": "3.1.0",
"guard-agent-rs": "3.1.0",
"guard-agent-ts": "3.1.0"
}
}
The two sides agree above because this release vendored its docs from those same versions, but they are independent and often will not: installed reflects this project's actual dependencies, while docs_bundled_for is fixed at build time to whatever scripts/sync_docs.py last vendored. Either side trailing the other is the normal case, not a fault. A null under installed is the real warning sign, a version that merely differs from docs_bundled_for is not. See Installation for the full explanation.
validate_config¶
| Parameter | Type | Default | Description |
|---|---|---|---|
config |
dict[str, Any] |
required | The config dict to validate |
package |
str |
"fastapi-guard" |
One of fastapi-guard, guard-core, guard-agent |
Validates config against the installed library's real Pydantic model (SecurityConfig for fastapi-guard/guard-core, AgentConfig for guard-agent) and reports four separate kinds of problems: unknown keys pydantic would otherwise silently ignore (with typo suggestions), validation errors, DeprecationWarnings the model raises for fields that still work but shouldn't be used, and, under construction_warnings, guard-core's own logger.warning signals for construction-time misconfigurations it does not raise on: an unknown constructor keyword, a trusted_proxies /0 network, a whitelist /0 network, and an empty enabled_detection_categories with detection enabled.
A deprecated field: as of guard-core 3.17.0, no SecurityConfig field is deprecated, so deprecated is [] for any valid config today. The shape is unchanged from before; a future deprecation would appear as an entry with the field's name and the model's own DeprecationWarning message, for example:
{
"field": "some_field",
"message": "some_field is deprecated and will be removed in a future release; ..."
}
Example call, a typo'd field:
Example response:
{
"valid": false,
"package": "fastapi-guard",
"version": "7.8.2",
"model": "SecurityConfig",
"errors": [],
"unknown_fields": [
{
"name": "enable_rate_limit",
"did_you_mean": ["enable_rate_limiting", "enable_rate_limit_auto_ban"]
}
],
"deprecated": [],
"construction_warnings": [
"SecurityConfig received unknown field 'enable_rate_limit'; it was ignored and had no effect. Did you mean 'enable_rate_limiting'?"
]
}
enable_rate_limit isn't a SecurityConfig field, the real one is enable_rate_limiting, and Pydantic would have accepted and then ignored it silently, which is why the same problem also shows up in construction_warnings: guard-core logs it as a logger.warning on construction. A type error (for example {"rate_limit": "not-a-number"}) instead produces an entry in errors, each with field, message, and the offending input.
config_fields¶
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
str |
required | An exact field name, or free text to match against field names and descriptions |
package |
str |
"fastapi-guard" |
One of fastapi-guard, guard-core, guard-agent |
An exact field name populates exact with that field's type, default, required-ness and description. Every query, exact or not, is also matched (case-insensitively, every token must appear) against every other field's name and description to populate matches; if that yields nothing and there was no exact hit either, matches falls back to the closest field names by fuzzy string distance.
Example call:
Example response:
{
"package": "fastapi-guard",
"version": "7.8.2",
"query": "rate_limit",
"exact": {
"name": "rate_limit",
"type": "int",
"default": "10",
"required": false,
"description": "Maximum requests per rate_limit_window"
},
"matches": [
{
"name": "threat_ban_config",
"type": "mappingproxy[str, ThreatBanConfig]",
"default": null,
"required": false,
"description": "Per-category ban thresholds and durations. Categories are the penetration-detection categories plus the pseudo-category 'rate_limit' (used only when enable_rate_limit_auto_ban is on). Unlisted categories fall back to auto_ban_threshold / auto_ban_duration."
},
{
"name": "enable_rate_limit_auto_ban",
"type": "bool",
"default": "False",
"required": false,
"description": "Feed rate-limit violations into the same auto-ban engine used for penetration detection: each active-mode (non-passive) violation increments the 'rate_limit' category of the existing suspicious-count structure and runs the same threshold logic (threat_ban_config['rate_limit'] override first, then the flat auto_ban_threshold / auto_ban_duration). Requires enable_ip_banning to actually ban. Default off: zero behavior change unless enabled."
},
{
"name": "rate_limit_window",
"type": "int",
"default": "60",
"required": false,
"description": "Rate limiting time window (seconds)"
},
{
"name": "enable_rate_limiting",
"type": "bool",
"default": "True",
"required": false,
"description": "Enable/disable rate limiting functionality"
},
{
"name": "endpoint_rate_limits",
"type": "dict[str, tuple[int, int]]",
"default": null,
"required": false,
"description": "Per-endpoint rate limits set by dynamic rules"
}
]
}
matches here still lists every other field whose name or description contains rate_limit, alongside the exact hit, which is how this doubles as "does a setting for X exist at all". A typo'd query with no token match at all, "rate_limti", say, falls through to the fuzzy fallback instead, which is matches populated purely from difflib.get_close_matches against every field name, with exact still null.
search_docs¶
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
str |
required | Free-text search terms |
package |
str \| None |
None |
One of the packages above; omit to search everything |
limit |
int |
5 |
Maximum number of results |
Searches both bundled corpora (guard_core_mcp/_docs/ for the vendored MkDocs/Astro sites, guard_core_mcp/_knowledge/ for the hand-written ecosystem entries) by counting query-term occurrences per page, and returns the highest-scoring pages with the best-matching heading and a snippet. Each result carries url, the citation URL for that page, built from the manifest recorded when the corpus was vendored, this works even when the underlying library isn't installed, since the corpora ship inside the wheel.
Example call:
Example response:
{
"query": "rate limiting",
"results": [
{
"package": "fastapi-guard",
"path": "tutorial/decorators/rate-limiting.md",
"heading": "",
"snippet": "description: Learn how to use rate limiting decorators for custom request rate controls and geographic rate limiting",
"url": "https://guard-core.github.io/fastapi-guard/latest/tutorial/decorators/rate-limiting/",
"score": 115
},
{
"package": "fastapi-guard",
"path": "release-notes.md",
"heading": "",
"snippet": "- **Geographic rate limit check**: Fixed geo-based rate limiting by implementing the missing `_check_geo_rate_limit` method in `RateLimitCheck`. Previously, geo rate limits configured via the `@security.geo_rate_limit` decorator were stored but never enforced. The rate limit pipeline now correctly e",
"url": "https://guard-core.github.io/fastapi-guard/latest/release-notes/",
"score": 92
},
{
"package": "fastapi-guard",
"path": "tutorial/ip-management/rate-limiter.md",
"heading": "",
"snippet": "Rate limiting is a crucial security feature that protects your API from abuse, DoS attacks, and excessive usage. FastAPI Guard provides a robust rate limiting system through the dedicated `RateLimitManager` class.",
"url": "https://guard-core.github.io/fastapi-guard/latest/tutorial/ip-management/rate-limiter/",
"score": 69
}
]
}
path and package from a result feed directly into get_doc.
get_doc¶
| Parameter | Type | Default | Description |
|---|---|---|---|
package |
str |
required | One of the packages versions reports under docs_bundled_for or knowledge_bundled_for |
path |
str |
required | A relative path from a search_docs result, e.g. installation.md |
Returns the full text of one bundled documentation page. path is resolved relative to that package's vendored root and rejected, as {"error": "unknown doc path"}, if it would escape that root or doesn't exist, so this cannot be used to read arbitrary files.
Example call:
Example response (content truncated here; the real response returns the full page)
{
"package": "fastapi-guard",
"path": "installation.md",
"url": "https://guard-core.github.io/fastapi-guard/latest/installation/",
"content": "---\n\ntitle: Installation - FastAPI Guard\ndescription: Learn how to install and set up FastAPI Guard, a comprehensive security middleware for FastAPI applications\nkeywords: fastapi guard installation, python security middleware, fastapi security setup\n---\n\nInstallation\n============\n\nInstall `fastapi-..."
}
check_payload¶
async def check_payload(
path: str = "/",
method: str = "GET",
query: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
body: str | dict[str, Any] | list[Any] | None = None,
config: dict[str, Any] | None = None,
) -> dict[str, Any]
| Parameter | Type | Default | Description |
|---|---|---|---|
path |
str |
"/" |
Request path |
method |
str |
"GET" |
HTTP method |
query |
dict[str, str] \| None |
None |
Query parameters |
headers |
dict[str, str] \| None |
None |
Request headers |
body |
str \| dict \| list \| None |
None |
Request body. A raw string is sent as-is; a JSON object or array is serialized for you |
config |
dict[str, Any] \| None |
None |
SecurityConfig fields to test how a setting changes the verdict |
The only async tool. Builds a synthetic request from the arguments and runs it through guard-core's real detect_penetration_attempt, using a SecurityConfig built from config (defaulting to guard-core's defaults) with enable_redis always forced to False, the sandbox never touches Redis, so results are Redis-independent by construction and a caller cannot re-enable it. elapsed_ms reflects actual detection time for that call and varies between runs; the values below are one real sample, not a promise. headers is matched case-insensitively, the same way guard-core's own GuardRequest protocol requires. A config value that fails SecurityConfig validation returns {"error": "invalid config", "errors": [...]}, with each entry in the same field/message/input shape validate_config reports for the identical failure.
guard-core 3.15.0 bounds the scan three ways: detection_max_scan_values caps the request values inspected (default 512, names and values counted), detection_max_scan_chars separately caps the total characters handed to the pattern engine across those values (default 65536), and a value that would start after either budget is spent is skipped, so a payload beyond either cap only gets a verdict on the scanned prefix. detection_max_json_depth (default 32) caps how deep a JSON body is walked structurally; a dict or list reached at that depth is serialized back to text and scanned as one value instead of being descended into further.
Since guard-core 3.15.0, detect_penetration_attempt also configures guard-core's detection singleton from the config it is given, the first time it runs or whenever the config object changes, instead of requiring the caller to configure it separately first. check_payload never configured that singleton itself, so before 3.15.0 it always ran guard-core's slower legacy pattern path rather than the enhanced path a real adapter runs, and could report a different verdict than a live request would. From 3.15.0 on, check_payload's verdicts come from that same enhanced path, this is the first guard-core-mcp release where that is true.
This is the detection stage, not the whole pipeline
check_payload runs detect_penetration_attempt and nothing else. A real request
also passes IP rules, rate limiting, user-agent, cloud-provider and route checks, any
of which can block it before detection ever runs. In particular, guard-core skips
detection entirely for a whitelisted IP, suspicious_activity.check() returns early
when request.state.is_whitelisted is set, so an app with a whitelist configured
will happily serve a payload this tool reports as a threat. Read a clean verdict as
"the detection engine does not flag this", not "the request reaches your route".
Example call, a SQL injection payload:
Example response:
{
"is_threat": true,
"trigger_info": "Query param 'q': Value matched pattern '(?i)(?:OR|AND)\\s+(?:'[\\w\\d]*'='[\\w\\d]*'?|[@:$][A-Za-z_]\\w*\\s*=\\s*[@:$][A-Za-z_]\\w*)'",
"threat_categories": ["sqli"],
"threat_scores": {"sqli": 1.0},
"elapsed_ms": 9.92
}
Example call, a benign request:
Example response: