#[non_exhaustive]pub enum ConfigValidationError {
Show 23 variants
EmptyServerName,
EmptyServerVersion,
EmptyToolName(usize),
EmptyTableName(usize),
InlineSecretRejected,
AmbiguousToolKind(usize),
EmptyBackendBaseUrl,
MalformedBackendBaseUrlRef,
MalformedBackendAuthRef(String),
EmptyConfigSlotField(usize),
SecretSlotCarriesTestedValue(usize),
UncompilableParamSchema {
tool: String,
position: String,
detail: String,
},
EmptyParamPattern {
tool: String,
param: String,
},
NonFiniteParamBound {
tool: String,
param: String,
},
UncappedStringParam {
tool: String,
param: String,
},
MalformedPathTemplateSegment {
tool: String,
segment: String,
},
CodeModeKeyWrongBackend {
key: &'static str,
server_kind: &'static str,
hint: &'static str,
},
ClassModeNeedsList {
class: &'static str,
mode: &'static str,
list: &'static str,
},
EmptyOperationField(usize),
DuplicateOperationId(String),
UnknownAutoApproveLevel(String),
ClassKeysUnenforceable(&'static str),
CuratedToolRefusedByPolicy {
tool: String,
reason: String,
},
}Expand description
Semantic-validation errors surfaced by
crate::config::ServerConfig::validate.
Per Phase 83 review R8 — the Default impls on ServerConfig and its
sub-sections deliberately allow from_toml to succeed even when required
fields are missing (so partial configs can be merged programmatically). The
crate::config::ServerConfig::validate entry-point catches these gaps at
parse time and surfaces them as a typed enum variant per rule.
The enum is #[non_exhaustive] — match callers must include a wildcard arm
so additional rules can be added without a breaking change.
§Examples
use pmcp_server_toolkit::ConfigValidationError;
// Each variant has a precise `Display` describing the rule violated.
let err = ConfigValidationError::EmptyServerName;
assert_eq!(err.to_string(), "server.name must be non-empty");
let err = ConfigValidationError::EmptyToolName(3);
assert_eq!(err.to_string(), "[[tools]] entry at index 3 has empty name");Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
EmptyServerName
[server] name is missing or whitespace-only.
EmptyServerVersion
[server] version is missing or whitespace-only.
EmptyToolName(usize)
[[tools]] entry at index has an empty / whitespace-only name.
EmptyTableName(usize)
[[database.tables]] entry at index has an empty / whitespace-only name.
InlineSecretRejected
Per Phase 83 Plan 06 review R9: [code_mode].token_secret was given as
an inline literal (e.g. token_secret = "raw-string") instead of the
env:VAR_NAME reference form, and the dev-only escape hatch
allow_inline_token_secret_for_dev was not set. Inline literals in
committed configs leak HMAC signing keys; the toolkit defaults to
rejecting them.
AmbiguousToolKind(usize)
Per Phase 90 Plan 02 (D-01, T-90-02-04): a [[tools]] entry declares
more than one mutually-exclusive tool kind. A tool is EITHER a SQL tool
(sql), a single-call HTTP tool (path/method), OR a script tool
(script) — never a mixture. The ambiguity is rejected rather than
resolved by a silent precedence rule. The usize is the entry index.
EmptyBackendBaseUrl
Per Phase 90 gap-closure (GAP 3 / WR-02): a [backend] block is present
but its base_url is empty / whitespace-only (or the base_url key was
omitted, defaulting to "" via #[serde(default)]). Without this
parse-time check a typo’d or missing base_url would validate cleanly
and then surface late as an opaque DispatchError::Connector("invalid base URL") at the first backend request. Rejecting it here turns that
late opaque failure into an actionable, field-naming error.
MalformedBackendBaseUrlRef
[backend].base_url is REFERENCE-shaped but does not name exactly one
environment variable — the empty ${} form, or a multi-placeholder
composition like "${SCHEME}://${HOST}". The grammar
(crate::env_ref::parse_env_ref) resolves one whole-value ${VAR} /
env:VAR reference; it does not interpolate inside a larger string, so
no environment could ever satisfy such a value. Without this check the
config loads cleanly and every boot fails with an
UnresolvedBaseUrlRef naming an empty variable.
MalformedBackendAuthRef(String)
A [backend.auth] credential field is REFERENCE-shaped but does not name
exactly one environment variable — the empty ${} form, a
multi-placeholder composition like "${SCHEME}://${HOST}", or a
non-portable name like "${TFL-APP-KEY}" (a ${...} name must match
[A-Za-z0-9_]+; env:VAR remains the escape hatch for exotic names).
The String is the offending field path within [backend.auth] — e.g.
"token", "password", "query_params.app_key".
This is the credential sibling of Self::MalformedBackendBaseUrlRef,
and it exists because the two paths resolve UNSET references differently
on purpose: a credential resolves an unset variable to the empty string
so an optional credential is OMITTED. A MALFORMED reference is not an
unset variable — no environment can ever satisfy it — so applying the
omission rule to it silently sent every backend request UNAUTHENTICATED,
with no error and no log line. Refusing it at load time turns that into
an actionable, field-naming failure before the server ever boots.
The message names the FIELD only and deliberately never echoes the configured value: a malformed value is by definition not a resolvable reference, so it may well be a mistyped literal secret.
EmptyConfigSlotField(usize)
Per Phase 120 Plan 04 (PKG-03): a [[config_slots]] entry at index
has an empty / whitespace-only key or name. A slot declaration whose
key names no config path — or whose name names no environment variable —
claims coverage it cannot deliver, and the package side would compare
against an empty string.
The sibling “unrecognized kind” check is NOT here: kind is the
closed crate::config::ConfigSlotKind enum, so serde rejects an
unknown discriminator at PARSE time (naming the accepted set) before
validate() is ever called.
SecretSlotCarriesTestedValue(usize)
A [[config_slots]] entry at index is kind = "secret" but carries a
tested_value. Identity-bearing slots structurally carry no value — the
tested_value field on a secret declaration is the one place a REAL
credential could sit in a config that is served but never packed (the
pack-time agreement gate only runs on packaging), so the rule is
enforced at validation time rather than trusted as prose. The message
deliberately does not echo the value.
UncompilableParamSchema
Per Phase 128 SC-2: a [[tools]] entry’s synthesized inputSchema does
not compile as a Draft 2020-12 schema — in practice always a
[[tools.parameters]] pattern that is not a valid regular expression.
Caught at CONFIG time rather than at call time, because a single
non-compiling pattern fails the whole document: the tool’s validator
never builds, so every call to it is refused (or, if the compile error were
swallowed, every call passes unchecked). Neither outcome should first be
discovered by a client.
§Why quoting detail is safe here
detail is the engine’s own compile-error text, which quotes the offending
SCHEMA — author-supplied config, never caller-supplied argument data. This
error is raised by crate::config::ServerConfig::validate, which runs at
load time with no request in scope, and its audience is the config author,
who needs the detail to fix the regex. The SC-7 no-echo rule governs the
CLIENT-facing tools/call refusal path, where a non-compiling schema still
yields a detail-free message.
position is the compile error’s JSON schema path — e.g.
/properties/region/pattern — so it names the offending parameter directly.
Fields
EmptyParamPattern
Per Phase 128 SC-2: a [[tools.parameters]] pattern was declared as the
empty string.
An empty pattern is a valid regular expression that matches every input,
so it buys no enforcement at all while reading — in a config review, in a
diff — exactly like a rule. Refused as a likely author error rather than
accepted as a no-op.
Fields
NonFiniteParamBound
Per Phase 128 D3: a [[tools.parameters]] minimum or maximum is
non-finite (NaN / infinity) or has a magnitude EXCEEDING 2^53.
§What this establishes, precisely
crate::config::ParamDecl::minimum and
crate::config::ParamDecl::maximum are f64. A TOML integer above 2^53
has therefore ALREADY been rounded by the time this check runs, so the check
cannot see that rounding happened and does NOT promise to catch a bound
sitting one unit past the boundary. It catches the non-finite and the wildly
out-of-range cases, which is where a silently-mangled bound is most likely
to be load-bearing.
The honest contract, stated on the field itself as well: minimum /
maximum are not a safe way to bound a 64-bit integer ID. Use a pattern
over the string form for that.
Fields
UncappedStringParam
Per Phase 128 D3 / D-07: a string parameter declares no max_length AND no
default cap reaches it, and [server.validation] strict = true promotes
that lint finding into a hard failure.
Deliberately NOT described as “body-position”. That is the usual case but not
the only one: [server.validation] default_max_length = 0 is a supported
opt-out that switches the D3 cap off for EVERY position, so a PATH or QUERY
parameter reaches this variant too. Naming a position the variant does not
actually pin sent the operator looking at the wrong parameter. The position
and the reason are carried by the paired
crate::config::ServerConfig::lint finding, which has both in scope.
Only reachable under strict. With strict = false (the default) the same
config validates cleanly and the finding is reported by lint instead — a
running server must never refuse to boot over an uncapped free-text field,
which is the whole reason the lint channel exists separately from validate.
Fields
MalformedPathTemplateSegment
Per Phase 128 D4(b): a single-call [[tools]] path carries a
/-delimited segment that is not a supported placeholder shape.
§The supported shape, and why anything else is an author error
On the curated single-call surface a placeholder is a WHOLE segment: the
path_placeholder_names helper recognizes {name} spanning an
entire /-delimited segment and nothing else. A segment that contains a
brace but is not exactly {name} therefore takes one of two bad routes at
call time, neither of which is what the author meant:
/search/{a}{b}parses to the single parameter namea}{b, which no[[tools.parameters]]entry can match, so nothing is substituted;/prefix-{id}is not recognized as carrying a placeholder at all, so the literal text{id}is what would travel toward the backend.
Both used to pass config validation and fail obscurely later. Refusing here turns a silently-wrong request into a startup error naming the segment. The segment text is author-written configuration, so echoing it is safe and is what makes the error actionable — it carries no caller data.
Fields
CodeModeKeyWrongBackend
A [code_mode] key that only means something on the other kind of
server: a SQL key on an OpenAPI server (one with a [backend]), or an
operation-class key on a server with no [backend]. It would be
silently ignored, so the server refuses to boot instead.
Fields
ClassModeNeedsList
A class mode that needs a list that is empty: allowlist with no
allowed_operations, or blocklist with no blocked_operations.
Fields
EmptyOperationField(usize)
A [[code_mode.operations]] entry with an empty id or path
(index into the list).
DuplicateOperationId(String)
Two [[code_mode.operations]] entries share an id.
UnknownAutoApproveLevel(String)
A [code_mode] auto_approve_levels entry that is not low, medium,
high or critical. It used to be skipped, which made a typo read as
“nothing auto-approved”.
ClassKeysUnenforceable(&'static str)
Operation-class keys were set, but this build cannot enforce them (the
openapi-code-mode feature is off). Raised by the HTTP tool synthesizer
when it would otherwise serve curated tools unchecked, never by
crate::config::ServerConfig::validate, whose verdict does not depend on
build features.
CuratedToolRefusedByPolicy
A curated [[tools]] entry calls an operation the [code_mode] class
policy refuses. The tool could never succeed, and a reader of the policy
would assume it is blocked, so the contradiction fails the boot.
Trait Implementations§
Source§impl Debug for ConfigValidationError
impl Debug for ConfigValidationError
Source§impl Display for ConfigValidationError
impl Display for ConfigValidationError
Source§impl Error for ConfigValidationError
impl Error for ConfigValidationError
1.30.0 · Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl From<ConfigValidationError> for ToolkitError
impl From<ConfigValidationError> for ToolkitError
Source§fn from(source: ConfigValidationError) -> Self
fn from(source: ConfigValidationError) -> Self
Auto Trait Implementations§
impl Freeze for ConfigValidationError
impl RefUnwindSafe for ConfigValidationError
impl Send for ConfigValidationError
impl Sync for ConfigValidationError
impl Unpin for ConfigValidationError
impl UnsafeUnpin for ConfigValidationError
impl UnwindSafe for ConfigValidationError
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more