Skip to main content

pmcp_server_toolkit/
lib.rs

1// Originated from pmcp-run/built-in/shared/mcp-server-common (https://github.com/guyernest/pmcp-run)
2// Promoted to rust-mcp-sdk workspace as a public SDK crate for Phase 83.
3
4//! Runtime library for config-driven MCP servers.
5//!
6//! `pmcp-server-toolkit` lifts the operational glue that pmcp-run servers
7//! share — auth providers, secrets resolution, static resources/prompts,
8//! a `[[tools]]` synthesizer, and code-mode wiring — into the public SDK.
9//!
10//! Phase 83 ships the empty crate skeleton; subsequent plans land the
11//! functionality across these modules:
12//!
13//! - [`auth`] — `AuthProvider` implementations (`StaticAuthProvider`, `BearerAuthProvider`).
14//! - [`secrets`] — `SecretsProvider` trait + env/AWS implementations and the
15//!   `SecretValue` newtype that never leaks via `Debug`/`Display`/`Serialize`.
16//! - [`config`] — `ServerConfig` types with `#[serde(deny_unknown_fields)]` strictness.
17//! - [`prompts`] — `StaticPromptHandler` adapter for static prompt templates.
18//! - [`resources`] — `StaticResourceHandler` adapter for shipped resources.
19//! - [`tools`] — `synthesize_from_config` builder turning `[[tools]]` into runtime handlers.
20//! - [`sql`] — `SqlConnector` trait + dialect enum for backend-agnostic SQL toolkits.
21//! - [`builder_ext`] — `ServerBuilderExt` extension methods on `pmcp::ServerBuilder`.
22//! - [`code_mode`] *(feature `code-mode`)* — re-exports from `pmcp-code-mode` plus toolkit glue.
23//! - [`error`] — `ToolkitError` enum and the crate-level `Result<T>` alias.
24//!
25//! The public module set is locked by Phase 83 decision D-15. See the
26//! `.planning/phases/83-toolkit-core-lift-pmcp-server-toolkit/` design log for
27//! the architectural responsibility map and review notes.
28
29/// This toolkit build's own version, captured at compile time.
30///
31/// # Why this is public API (Phase 128, SC-3)
32///
33/// `ServerConfig::lint()` is the ONE implementation of the input-validation
34/// review rules, and both `cargo pmcp validate config` and a running server
35/// report from it. That makes the two surfaces agree for a **same-version**
36/// pair and says nothing about a mixed one: a config that lints clean under the
37/// toolkit a reviewer's CLI was BUILT against may lint dirty under the toolkit
38/// the deployed server RUNS. A clean lint is therefore a version-scoped
39/// statement, never an absolute guarantee about production.
40///
41/// So the lint surface prints which toolkit performed it, and this constant is
42/// what it prints. It is `env!("CARGO_PKG_VERSION")` rather than a string read
43/// from a manifest on disk, so it names the crate that is actually LINKED in and
44/// cannot drift from it.
45///
46/// Deliberately NOT a hard error on mismatch: the CLI has no way to know which
47/// toolkit the deployment will run, so refusing would be refusing on a guess.
48/// The operator gets the number and can compare it to what they deploy.
49pub const VERSION: &str = env!("CARGO_PKG_VERSION");
50
51pub mod auth;
52pub mod builder_ext;
53pub mod config;
54
55/// The `${VAR}` / `env:VAR` reference grammar — the ONE parse chokepoint every
56/// env-reference path in the toolkit shares (credentials, `token_secret`, and
57/// `[backend].base_url`).
58///
59/// Deliberately carries NO `#[cfg(feature = ...)]` gate: the function used to
60/// live inside the feature-gated `http` module, which made "the toolkit's
61/// universal chokepoint" true only in `http` builds.
62///
63/// `pub` rather than `pub(crate)` (plan 120-05): the grammar is duplicated by
64/// necessity in `pmcp-package` — the workspace-excluded leaf crate, which
65/// neither may depend on this one nor be depended on by it — and the two
66/// implementations are held to a shared accept/reject table asserted from an
67/// INTEGRATION test in each crate. An integration test is an external consumer,
68/// so the reference implementation has to be reachable from outside the crate
69/// for that parity claim to be checkable at all.
70pub mod env_ref;
71
72pub mod error;
73pub mod policy;
74pub mod prompts;
75pub mod resources;
76pub mod secrets;
77pub mod sql;
78pub mod tools;
79
80/// HTTP backend primitives for config-driven OpenAPI MCP servers (Phase 90).
81///
82/// Gated behind the opt-in `http` feature so the curated / no-`http` toolkit
83/// build stays light (RESEARCH Pitfall 4).
84#[cfg(feature = "http")]
85pub mod http;
86
87/// Governed-Excel workbook served-tool module (Phase 92).
88///
89/// Gated behind the opt-in `workbook` feature so the no-`workbook` toolkit
90/// build never links the `pmcp-workbook-runtime` BundleSource/BundleLoader
91/// surface. The `workbook-embedded` feature additionally enables the runtime's
92/// `embedded` (include_dir) EmbeddedSource for binary-baked bundles.
93#[cfg(feature = "workbook")]
94pub mod workbook;
95
96#[cfg(feature = "code-mode")]
97pub mod code_mode;
98
99pub use error::{Result, ToolkitError};
100
101// === Crate-root re-exports per D-15 (headline DX promise — reviewed R3) ===
102//
103// A Shape C consumer writes a single one-line crate-root import:
104//   use pmcp_server_toolkit::{AuthProvider, StaticAuthProvider,
105//                             SecretsProvider, SecretValue, EnvSecrets};
106//
107// NO `as _` no-name imports (those break the DX promise — review R3).
108
109// Auth — re-export pmcp's trait at the toolkit crate root so consumers don't
110// have to write `pmcp::server::auth::AuthProvider`. The toolkit's static impl
111// is also re-exported at crate root.
112pub use crate::auth::StaticAuthProvider;
113pub use pmcp::server::auth::AuthProvider;
114
115// Secrets — toolkit-owned trait + value type + concrete impls. Per review R6
116// the secret type `SecretValue` is toolkit-owned (NOT pmcp_code_mode::TokenSecret),
117// so it's stable under `--no-default-features`.
118pub use crate::secrets::{EnvSecrets, SecretValue, SecretsProvider, SecretsProviderChain};
119
120// AWS-feature-gated secrets impls.
121#[cfg(feature = "aws")]
122pub use crate::secrets::{OrgSecretsManagerProvider, SecretsManagerSecrets, SsmSecrets};
123
124// Resources (TKIT-04) — Plan 03 headline re-export per D-15 + review R3.
125pub use crate::resources::StaticResourceHandler;
126
127// Prompts (TKIT-05) — Plan 03 headline re-export per D-15 + review R3.
128pub use crate::prompts::StaticPromptHandler;
129
130// Plan 08 (TKIT-05 completion): the multi-prompt construction helper. The
131// `impl From<&ServerConfig>` on `StaticPromptHandler` covers single-prompt
132// servers; this function covers the common multi-prompt path. Lifted to the
133// crate root per review R3 so the backend-core smoke test and downstream
134// shape-C consumers don't need `pmcp_server_toolkit::prompts::*` paths.
135pub use crate::prompts::prompt_handlers_from_config;
136
137// Config (TKIT-01) — Plan 04 headline re-export per D-15 + review R3.
138// ServerConfig is THE single top-level config type a Shape C consumer touches.
139pub use crate::config::ServerConfig;
140
141// Validation error type also surfaces at the crate root so consumers can
142// pattern-match on it without importing from `error` (review R3 headline DX).
143pub use crate::error::ConfigValidationError;
144
145// Tools (TKIT-07) — Plan 05 headline re-export per D-15 + review R3.
146// `synthesize_from_config` is the one-call entry point Shape A/C consumers
147// reach for; lifting it to the crate root keeps the import surface flat.
148pub use crate::tools::synthesize_from_config;
149
150// Phase 84 (CONN-01 / D-06) — additive connector-threaded variant alongside the
151// existing `synthesize_from_config`. The no-connector entry point above is
152// unchanged; this one wires `Arc<dyn SqlConnector>` into each handler so
153// `tools/call` can execute SQL and emit `structuredContent`.
154pub use crate::tools::synthesize_from_config_with_connector;
155
156// Phase 90 (OAPI-02a) — single-call HTTP synthesizer, mirroring the SQL
157// connector-threaded variant above. Feature-gated on `http`. Wires
158// `Arc<dyn HttpConnector>` into each single-call `[[tools]]` handler so
159// `tools/call` executes the REST operation and returns JSON.
160#[cfg(feature = "http")]
161pub use crate::tools::synthesize_from_config_with_http_connector;
162
163// Phase 128 E2 — the hooks-carrying siblings of the two entry points above, at the
164// crate root for the same reason the originals are: a consumer's imports are ONE
165// crate-root block, and an integration test is an external consumer.
166#[cfg(feature = "http")]
167pub use crate::tools::synthesize_from_config_with_http_connector_and_hooks;
168pub use crate::tools::{
169    synthesize_from_config_and_hooks, synthesize_from_config_with_connector_and_hooks,
170};
171
172// Phase 90 (OAPI-02b / D-01 / D-02) — single-call + SCRIPT HTTP synthesizer.
173// Gated `openapi-code-mode` (the umbrella that forwards
174// `pmcp-code-mode/js-runtime`). Adds the shared `HttpCodeExecutor` + bounds so a
175// `script` `[[tools]]` synthesizes a `ScriptToolHandler` that runs admin-authored
176// JS over the SAME engine Code Mode uses (one engine, two surfaces).
177#[cfg(feature = "openapi-code-mode")]
178pub use crate::tools::synthesize_from_config_with_http_connector_and_scripts;
179
180// Phase 128 E2 — the hooks-carrying variant `pmcp-openapi-server`'s `build_server`
181// calls. Re-exported at the crate root alongside the variant above, because that
182// binary is a DIFFERENT crate and this is its only route to registering an
183// `ArgumentValidator` (T-128-39b).
184#[cfg(feature = "openapi-code-mode")]
185pub use crate::tools::synthesize_from_config_with_http_connector_and_scripts_and_hooks;
186
187// Builder extensions (TKIT-08) — Plan 08 headline re-export per D-15 + review R3.
188// The trait method set is the Shape C ≤15-line `main.rs` surface; lifting it
189// to the crate root is the binding witness of D-15 (the runnable example
190// imports SOLELY from `pmcp_server_toolkit::*` — never from module paths).
191pub use crate::builder_ext::ServerBuilderExt;
192
193// SQL connector trait stub (TKIT-10) — Plan 07 headline re-export per D-15 +
194// review R3. MINIMIZED Phase 83 surface per review R2: ONLY `Dialect`,
195// `SqlConnector`, and `ConnectorError` are re-exported. `execute()` and
196// `translate_placeholders` are intentionally absent — they land in Phase 84
197// (pmcp-server-toolkit 0.2.0) once the first real connector validates the
198// contract. `MockSqlConnector` stays `pub(crate)` — it's test-only.
199pub use crate::sql::{ConnectorError, Dialect, SqlConnector};
200
201// Phase 128 E1/E2 escape hatches — the FULL registration surface at the crate
202// root, deliberately not feature-gated. A toolkit example's imports are ONE
203// crate-root block (D-15), and this is the re-export that keeps it so: if an
204// example cannot name `RequestPolicy` this way the fix is here, never a
205// module-path-qualified import in the example.
206//
207// `policy` carries no `#[cfg]` because `ToolkitHooks` is a parameter of the
208// always-present `ServerBuilderExt::try_tools_from_config_with`; gating it on
209// `http` would make the registration surface exist only in HTTP builds while the
210// E2 half has nothing to do with HTTP.
211/// The `#[async_trait]` attribute, re-exported so an out-of-crate implementor of
212/// [`RequestPolicy`] (or [`http::auth::HttpAuthProvider`]) does not have to add an
213/// `async-trait` dependency of its own — and, more importantly, cannot end up on a
214/// DIFFERENT version of it than the trait was declared with, which produces a
215/// signature-mismatch error that reads as a lifetime bug.
216pub use async_trait::async_trait;
217
218pub use crate::policy::{
219    emit_validation_report, render_validation_report, ArgumentRefusal, ArgumentValidator,
220    OutboundRequest, PolicyRefusal, ReportLevel, ReportLine, RequestPhase, RequestPolicy,
221    ToolkitHooks,
222};
223
224// HTTP connector (Phase 90 OAPI-01) — crate-root re-export of the headline
225// types, mirroring the SQL connector re-export. Feature-gated on `http`.
226#[cfg(feature = "http")]
227pub use crate::http::{HttpConnector, HttpConnectorError, Operation};
228
229// Phase 128 — the rest of what an E1 example needs to build a governed connector in
230// ONE crate-root import block (D-15). `HttpClient` is the connector a
231// `RequestPolicy` is attached to, and the auth pair is what makes the
232// "the policy never sees the credential" demonstration meaningful: without a real
233// credential in play, a clean scan proves nothing.
234#[cfg(feature = "http")]
235pub use crate::http::auth::{create_auth_provider, AuthConfig};
236#[cfg(feature = "http")]
237pub use crate::http::HttpClient;
238
239// Workbook served-tool boot surface (Phase 92, WBSV-01/08/09 / D-11) — the
240// FULL consumer-side contract at the crate root so Shape A/B servers register a
241// governed workbook in ONE call WITHOUT ever naming `pmcp-workbook-runtime`:
242// the builder-ext trait, the `BundleSource` trait + its on-disk impl, the
243// fail-closed loader entry point, and both error types. The `EmbeddedSource`
244// impl is gated on `workbook-embedded` (it needs the runtime's `embedded`
245// include_dir support). Gated on `workbook` because the module is feature-gated.
246#[cfg(feature = "workbook")]
247pub use crate::workbook::{
248    load_bundle, BundleLoadError, BundleSource, BundleSourceError, LocalDirSource,
249    WorkbookBuilderExt,
250};
251
252/// The binary-baked workbook [`BundleSource`] (WBSV-09), re-exported at the
253/// crate root only when the `workbook-embedded` feature is active.
254#[cfg(feature = "workbook-embedded")]
255pub use crate::workbook::EmbeddedSource;
256
257// Code-mode prompt assembler (TKIT-10 / D-12) — Plan 07 headline re-export.
258// Feature-gated on `code-mode` because it lives in the code_mode module which
259// is itself feature-gated (D-16: code-mode is opt-in).
260#[cfg(feature = "code-mode")]
261pub use crate::code_mode::assemble_code_mode_prompt;
262
263// File-based prompt seam (Plan 85-02 Task 3 / D-04 / D-05) — the sync,
264// connectorless counterpart that seeds the prompt from a `--schema` file
265// without live introspection (SC-1 prerequisite).
266#[cfg(feature = "code-mode")]
267pub use crate::code_mode::assemble_code_mode_prompt_with_schema;
268
269// === Asset-aware path resolution (Phase 86 Review H1 — decided ONCE) ===
270//
271// Shapes B/C/D (the example, the scaffold emitter, and the deploy path) all need
272// the SAME answer to "where do I read config.toml / schema.sql, and where do I
273// write the demo SQLite DB?" so that a generated `main.rs` runs unchanged locally
274// AND on AWS Lambda. The resolution is fixed here and re-used everywhere; callers
275// MUST NOT hand-roll path logic.
276//
277// Config + schema are loaded via `pmcp::assets::load_string("config.toml")` and
278// `pmcp::assets::load_string("schema.sql")`. The pmcp asset loader already
279// resolves the correct base on each platform (verified in `src/assets/loader.rs`):
280//   - Lambda: `$LAMBDA_TASK_ROOT/assets` (default `/var/task/assets`) — the
281//     deploy bundler places `[assets] include` files under `assets/` in the zip.
282//   - Local: `$PMCP_ASSETS_DIR` or the current working directory.
283// The `assets` module is NOT feature-gated, so it is reachable from the toolkit's
284// `default-features = false` `pmcp` dependency without enabling extra features.
285
286/// Resolve the writable filesystem path for the demo SQLite database.
287///
288/// On AWS Lambda the deployment root (`/var/task`) is read-only, so a SQLite
289/// database that must be created/seeded at startup has to live under the
290/// writable `/tmp`. Locally a relative `demo.db` in the working directory is
291/// fine. Lambda is detected by the presence of the `LAMBDA_TASK_ROOT`
292/// environment variable, which the Lambda runtime always sets.
293///
294/// This pairs with `pmcp::assets::load_string("config.toml")` /
295/// `pmcp::assets::load_string("schema.sql")` for read-only assets — config and
296/// schema are bundled (and resolved) via the pmcp asset loader, while the
297/// mutable database goes wherever this resolver points. Both halves are decided
298/// once here so the example, the scaffold emitter, and the deploy path share one
299/// shape (Phase 86 Review H1).
300///
301/// # Examples
302///
303/// ```
304/// use pmcp_server_toolkit::demo_db_path;
305///
306/// // Locally (no LAMBDA_TASK_ROOT) the demo DB is a relative file.
307/// std::env::remove_var("LAMBDA_TASK_ROOT");
308/// assert_eq!(demo_db_path(), std::path::PathBuf::from("demo.db"));
309/// ```
310#[must_use]
311pub fn demo_db_path() -> std::path::PathBuf {
312    if std::env::var("LAMBDA_TASK_ROOT").is_ok() {
313        // Lambda: /var/task is read-only; SQLite must bootstrap into /tmp.
314        std::path::PathBuf::from("/tmp/demo.db")
315    } else {
316        std::path::PathBuf::from("demo.db")
317    }
318}
319
320// Why: compile-only assertion proving the headline D-15 / review-R3 crate-root
321// DX promise. If any of these paths fails to resolve, the crate fails to
322// build — no test runtime required.
323#[allow(dead_code)]
324const _ROOT_REEXPORT_SMOKE: fn() = || {
325    let _: Option<&dyn AuthProvider> = None;
326    let _: Option<&dyn SecretsProvider> = None;
327    let _: Option<StaticAuthProvider> = None;
328    let _: Option<EnvSecrets> = None;
329    let _: Option<SecretValue> = None;
330    let _: Option<SecretsProviderChain> = None;
331    let _: Option<StaticResourceHandler> = None;
332    let _: Option<StaticPromptHandler> = None;
333    let _: Option<ServerConfig> = None;
334    let _: Option<ConfigValidationError> = None;
335    // Plan 05 (TKIT-07): synthesize_from_config is fn-typed; reference the
336    // function pointer to assert the re-exported path resolves at the crate root.
337    let _: fn(&ServerConfig) -> Result<Vec<crate::tools::SynthesizedTool>> = synthesize_from_config;
338    // Plan 07 (TKIT-10): SqlConnector trait stub + Dialect enum re-exports.
339    let _: Option<Dialect> = None;
340    let _: Option<ConnectorError> = None;
341    let _: Option<&dyn SqlConnector> = None;
342    // Plan 08 (TKIT-08): ServerBuilderExt trait — the headline Shape C
343    // surface. The trait is `Sized` (can't be `dyn`) — reference its method
344    // pointer instead to assert the crate-root path resolves.
345    let _: fn(pmcp::ServerBuilder, &ServerConfig) -> Result<pmcp::ServerBuilder> =
346        <pmcp::ServerBuilder as ServerBuilderExt>::try_tools_from_config;
347};
348
349// Plan 06 (TKIT-06 + TKIT-09): compile-only assertion that the code_mode
350// submodule's re-exports + wiring helpers resolve at `code_mode::*`. Gated on
351// `code-mode` because the module itself is feature-gated (D-15 + D-16: the
352// headline submodule, not a flattened crate-root surface).
353#[cfg(feature = "code-mode")]
354#[allow(dead_code)]
355const _CODE_MODE_REEXPORT_SMOKE: fn() = || {
356    let _: Option<Box<dyn crate::code_mode::CodeExecutor>> = None;
357    let _: Option<crate::code_mode::ValidationPipeline> = None;
358    let _: Option<crate::code_mode::TokenSecret> = None;
359    let _: Option<crate::code_mode::HmacTokenGenerator> = None;
360    let _: Option<crate::code_mode::ApprovalToken> = None;
361    let _: Option<crate::code_mode::NoopPolicyEvaluator> = None;
362    let _: fn(&ServerConfig) -> Result<crate::code_mode::ValidationPipeline> =
363        crate::code_mode::validation_pipeline_from_config;
364    // Plan 07 (TKIT-10 / D-12): assemble_code_mode_prompt re-exports at crate
365    // root under the code-mode feature. The fn returns a `BoxFuture`-ish async
366    // surface; reference the function pointer to assert the path resolves.
367    let _ = assemble_code_mode_prompt;
368};
369
370// Phase 92 (WBSV-01/08/09 / D-11): compile-only assertion that the FULL workbook
371// boot surface resolves at the crate root — the binding witness that a Shape A/B
372// consumer registers a governed workbook WITHOUT naming `pmcp-workbook-runtime`.
373// Gated on `workbook` because the module is feature-gated.
374#[cfg(feature = "workbook")]
375#[allow(dead_code)]
376const _WORKBOOK_REEXPORT_SMOKE: fn() = || {
377    // BundleSource (trait) + its on-disk impl + both error types — the loader
378    // inputs/outputs consumers need from the crate root.
379    let _: Option<&dyn BundleSource> = None;
380    let _: Option<LocalDirSource> = None;
381    let _: Option<BundleSourceError> = None;
382    let _: Option<BundleLoadError> = None;
383    // load_bundle (the fail-closed boot entry point) is fn-typed; reference its
384    // function pointer to assert the re-exported path resolves at the crate root.
385    let _: fn(
386        &dyn BundleSource,
387    ) -> std::result::Result<crate::workbook::WorkbookBundle, BundleLoadError> = load_bundle;
388    // WorkbookBuilderExt (the headline one-call registration) is `Sized` (can't
389    // be `dyn`) — reference its `try_` method pointer instead.
390    let _: fn(pmcp::ServerBuilder, &dyn BundleSource) -> Result<pmcp::ServerBuilder> =
391        <pmcp::ServerBuilder as WorkbookBuilderExt>::try_with_workbook_bundle;
392};
393
394// Phase 92 (WBSV-09): the embedded-source re-export resolves at the crate root
395// when the `workbook-embedded` feature layers include_dir support on top.
396#[cfg(feature = "workbook-embedded")]
397#[allow(dead_code)]
398const _WORKBOOK_EMBEDDED_REEXPORT_SMOKE: fn() = || {
399    let _: Option<EmbeddedSource> = None;
400};
401
402#[cfg(test)]
403mod demo_db_path_tests {
404    use super::demo_db_path;
405    use std::path::PathBuf;
406
407    // Why: these tests mutate the process-global LAMBDA_TASK_ROOT env var. The
408    // project runs `cargo test -- --test-threads=1` (CLAUDE.md), so they execute
409    // serially and cannot race. Each test restores the prior state.
410    #[test]
411    fn returns_tmp_path_under_lambda() {
412        let prev = std::env::var("LAMBDA_TASK_ROOT").ok();
413        std::env::set_var("LAMBDA_TASK_ROOT", "/var/task");
414        assert_eq!(demo_db_path(), PathBuf::from("/tmp/demo.db"));
415        match prev {
416            Some(v) => std::env::set_var("LAMBDA_TASK_ROOT", v),
417            None => std::env::remove_var("LAMBDA_TASK_ROOT"),
418        }
419    }
420
421    #[test]
422    fn returns_relative_path_locally() {
423        let prev = std::env::var("LAMBDA_TASK_ROOT").ok();
424        std::env::remove_var("LAMBDA_TASK_ROOT");
425        assert_eq!(demo_db_path(), PathBuf::from("demo.db"));
426        if let Some(v) = prev {
427            std::env::set_var("LAMBDA_TASK_ROOT", v);
428        }
429    }
430}