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
29pub mod auth;
30pub mod builder_ext;
31pub mod config;
32
33/// The `${VAR}` / `env:VAR` reference grammar — the ONE parse chokepoint every
34/// env-reference path in the toolkit shares (credentials, `token_secret`, and
35/// `[backend].base_url`).
36///
37/// Deliberately carries NO `#[cfg(feature = ...)]` gate: the function used to
38/// live inside the feature-gated `http` module, which made "the toolkit's
39/// universal chokepoint" true only in `http` builds.
40///
41/// `pub` rather than `pub(crate)` (plan 120-05): the grammar is duplicated by
42/// necessity in `pmcp-package` — the workspace-excluded leaf crate, which
43/// neither may depend on this one nor be depended on by it — and the two
44/// implementations are held to a shared accept/reject table asserted from an
45/// INTEGRATION test in each crate. An integration test is an external consumer,
46/// so the reference implementation has to be reachable from outside the crate
47/// for that parity claim to be checkable at all.
48pub mod env_ref;
49
50pub mod error;
51pub mod prompts;
52pub mod resources;
53pub mod secrets;
54pub mod sql;
55pub mod tools;
56
57/// HTTP backend primitives for config-driven OpenAPI MCP servers (Phase 90).
58///
59/// Gated behind the opt-in `http` feature so the curated / no-`http` toolkit
60/// build stays light (RESEARCH Pitfall 4).
61#[cfg(feature = "http")]
62pub mod http;
63
64/// Governed-Excel workbook served-tool module (Phase 92).
65///
66/// Gated behind the opt-in `workbook` feature so the no-`workbook` toolkit
67/// build never links the `pmcp-workbook-runtime` BundleSource/BundleLoader
68/// surface. The `workbook-embedded` feature additionally enables the runtime's
69/// `embedded` (include_dir) EmbeddedSource for binary-baked bundles.
70#[cfg(feature = "workbook")]
71pub mod workbook;
72
73#[cfg(feature = "code-mode")]
74pub mod code_mode;
75
76pub use error::{Result, ToolkitError};
77
78// === Crate-root re-exports per D-15 (headline DX promise — reviewed R3) ===
79//
80// A Shape C consumer writes a single one-line crate-root import:
81//   use pmcp_server_toolkit::{AuthProvider, StaticAuthProvider,
82//                             SecretsProvider, SecretValue, EnvSecrets};
83//
84// NO `as _` no-name imports (those break the DX promise — review R3).
85
86// Auth — re-export pmcp's trait at the toolkit crate root so consumers don't
87// have to write `pmcp::server::auth::AuthProvider`. The toolkit's static impl
88// is also re-exported at crate root.
89pub use crate::auth::StaticAuthProvider;
90pub use pmcp::server::auth::AuthProvider;
91
92// Secrets — toolkit-owned trait + value type + concrete impls. Per review R6
93// the secret type `SecretValue` is toolkit-owned (NOT pmcp_code_mode::TokenSecret),
94// so it's stable under `--no-default-features`.
95pub use crate::secrets::{EnvSecrets, SecretValue, SecretsProvider, SecretsProviderChain};
96
97// AWS-feature-gated secrets impls.
98#[cfg(feature = "aws")]
99pub use crate::secrets::{OrgSecretsManagerProvider, SecretsManagerSecrets, SsmSecrets};
100
101// Resources (TKIT-04) — Plan 03 headline re-export per D-15 + review R3.
102pub use crate::resources::StaticResourceHandler;
103
104// Prompts (TKIT-05) — Plan 03 headline re-export per D-15 + review R3.
105pub use crate::prompts::StaticPromptHandler;
106
107// Plan 08 (TKIT-05 completion): the multi-prompt construction helper. The
108// `impl From<&ServerConfig>` on `StaticPromptHandler` covers single-prompt
109// servers; this function covers the common multi-prompt path. Lifted to the
110// crate root per review R3 so the backend-core smoke test and downstream
111// shape-C consumers don't need `pmcp_server_toolkit::prompts::*` paths.
112pub use crate::prompts::prompt_handlers_from_config;
113
114// Config (TKIT-01) — Plan 04 headline re-export per D-15 + review R3.
115// ServerConfig is THE single top-level config type a Shape C consumer touches.
116pub use crate::config::ServerConfig;
117
118// Validation error type also surfaces at the crate root so consumers can
119// pattern-match on it without importing from `error` (review R3 headline DX).
120pub use crate::error::ConfigValidationError;
121
122// Tools (TKIT-07) — Plan 05 headline re-export per D-15 + review R3.
123// `synthesize_from_config` is the one-call entry point Shape A/C consumers
124// reach for; lifting it to the crate root keeps the import surface flat.
125pub use crate::tools::synthesize_from_config;
126
127// Phase 84 (CONN-01 / D-06) — additive connector-threaded variant alongside the
128// existing `synthesize_from_config`. The no-connector entry point above is
129// unchanged; this one wires `Arc<dyn SqlConnector>` into each handler so
130// `tools/call` can execute SQL and emit `structuredContent`.
131pub use crate::tools::synthesize_from_config_with_connector;
132
133// Phase 90 (OAPI-02a) — single-call HTTP synthesizer, mirroring the SQL
134// connector-threaded variant above. Feature-gated on `http`. Wires
135// `Arc<dyn HttpConnector>` into each single-call `[[tools]]` handler so
136// `tools/call` executes the REST operation and returns JSON.
137#[cfg(feature = "http")]
138pub use crate::tools::synthesize_from_config_with_http_connector;
139
140// Phase 90 (OAPI-02b / D-01 / D-02) — single-call + SCRIPT HTTP synthesizer.
141// Gated `openapi-code-mode` (the umbrella that forwards
142// `pmcp-code-mode/js-runtime`). Adds the shared `HttpCodeExecutor` + bounds so a
143// `script` `[[tools]]` synthesizes a `ScriptToolHandler` that runs admin-authored
144// JS over the SAME engine Code Mode uses (one engine, two surfaces).
145#[cfg(feature = "openapi-code-mode")]
146pub use crate::tools::synthesize_from_config_with_http_connector_and_scripts;
147
148// Builder extensions (TKIT-08) — Plan 08 headline re-export per D-15 + review R3.
149// The trait method set is the Shape C ≤15-line `main.rs` surface; lifting it
150// to the crate root is the binding witness of D-15 (the runnable example
151// imports SOLELY from `pmcp_server_toolkit::*` — never from module paths).
152pub use crate::builder_ext::ServerBuilderExt;
153
154// SQL connector trait stub (TKIT-10) — Plan 07 headline re-export per D-15 +
155// review R3. MINIMIZED Phase 83 surface per review R2: ONLY `Dialect`,
156// `SqlConnector`, and `ConnectorError` are re-exported. `execute()` and
157// `translate_placeholders` are intentionally absent — they land in Phase 84
158// (pmcp-server-toolkit 0.2.0) once the first real connector validates the
159// contract. `MockSqlConnector` stays `pub(crate)` — it's test-only.
160pub use crate::sql::{ConnectorError, Dialect, SqlConnector};
161
162// HTTP connector (Phase 90 OAPI-01) — crate-root re-export of the headline
163// types, mirroring the SQL connector re-export. Feature-gated on `http`.
164#[cfg(feature = "http")]
165pub use crate::http::{HttpConnector, HttpConnectorError, Operation};
166
167// Workbook served-tool boot surface (Phase 92, WBSV-01/08/09 / D-11) — the
168// FULL consumer-side contract at the crate root so Shape A/B servers register a
169// governed workbook in ONE call WITHOUT ever naming `pmcp-workbook-runtime`:
170// the builder-ext trait, the `BundleSource` trait + its on-disk impl, the
171// fail-closed loader entry point, and both error types. The `EmbeddedSource`
172// impl is gated on `workbook-embedded` (it needs the runtime's `embedded`
173// include_dir support). Gated on `workbook` because the module is feature-gated.
174#[cfg(feature = "workbook")]
175pub use crate::workbook::{
176    load_bundle, BundleLoadError, BundleSource, BundleSourceError, LocalDirSource,
177    WorkbookBuilderExt,
178};
179
180/// The binary-baked workbook [`BundleSource`] (WBSV-09), re-exported at the
181/// crate root only when the `workbook-embedded` feature is active.
182#[cfg(feature = "workbook-embedded")]
183pub use crate::workbook::EmbeddedSource;
184
185// Code-mode prompt assembler (TKIT-10 / D-12) — Plan 07 headline re-export.
186// Feature-gated on `code-mode` because it lives in the code_mode module which
187// is itself feature-gated (D-16: code-mode is opt-in).
188#[cfg(feature = "code-mode")]
189pub use crate::code_mode::assemble_code_mode_prompt;
190
191// File-based prompt seam (Plan 85-02 Task 3 / D-04 / D-05) — the sync,
192// connectorless counterpart that seeds the prompt from a `--schema` file
193// without live introspection (SC-1 prerequisite).
194#[cfg(feature = "code-mode")]
195pub use crate::code_mode::assemble_code_mode_prompt_with_schema;
196
197// === Asset-aware path resolution (Phase 86 Review H1 — decided ONCE) ===
198//
199// Shapes B/C/D (the example, the scaffold emitter, and the deploy path) all need
200// the SAME answer to "where do I read config.toml / schema.sql, and where do I
201// write the demo SQLite DB?" so that a generated `main.rs` runs unchanged locally
202// AND on AWS Lambda. The resolution is fixed here and re-used everywhere; callers
203// MUST NOT hand-roll path logic.
204//
205// Config + schema are loaded via `pmcp::assets::load_string("config.toml")` and
206// `pmcp::assets::load_string("schema.sql")`. The pmcp asset loader already
207// resolves the correct base on each platform (verified in `src/assets/loader.rs`):
208//   - Lambda: `$LAMBDA_TASK_ROOT/assets` (default `/var/task/assets`) — the
209//     deploy bundler places `[assets] include` files under `assets/` in the zip.
210//   - Local: `$PMCP_ASSETS_DIR` or the current working directory.
211// The `assets` module is NOT feature-gated, so it is reachable from the toolkit's
212// `default-features = false` `pmcp` dependency without enabling extra features.
213
214/// Resolve the writable filesystem path for the demo SQLite database.
215///
216/// On AWS Lambda the deployment root (`/var/task`) is read-only, so a SQLite
217/// database that must be created/seeded at startup has to live under the
218/// writable `/tmp`. Locally a relative `demo.db` in the working directory is
219/// fine. Lambda is detected by the presence of the `LAMBDA_TASK_ROOT`
220/// environment variable, which the Lambda runtime always sets.
221///
222/// This pairs with `pmcp::assets::load_string("config.toml")` /
223/// `pmcp::assets::load_string("schema.sql")` for read-only assets — config and
224/// schema are bundled (and resolved) via the pmcp asset loader, while the
225/// mutable database goes wherever this resolver points. Both halves are decided
226/// once here so the example, the scaffold emitter, and the deploy path share one
227/// shape (Phase 86 Review H1).
228///
229/// # Examples
230///
231/// ```
232/// use pmcp_server_toolkit::demo_db_path;
233///
234/// // Locally (no LAMBDA_TASK_ROOT) the demo DB is a relative file.
235/// std::env::remove_var("LAMBDA_TASK_ROOT");
236/// assert_eq!(demo_db_path(), std::path::PathBuf::from("demo.db"));
237/// ```
238#[must_use]
239pub fn demo_db_path() -> std::path::PathBuf {
240    if std::env::var("LAMBDA_TASK_ROOT").is_ok() {
241        // Lambda: /var/task is read-only; SQLite must bootstrap into /tmp.
242        std::path::PathBuf::from("/tmp/demo.db")
243    } else {
244        std::path::PathBuf::from("demo.db")
245    }
246}
247
248// Why: compile-only assertion proving the headline D-15 / review-R3 crate-root
249// DX promise. If any of these paths fails to resolve, the crate fails to
250// build — no test runtime required.
251#[allow(dead_code)]
252const _ROOT_REEXPORT_SMOKE: fn() = || {
253    let _: Option<&dyn AuthProvider> = None;
254    let _: Option<&dyn SecretsProvider> = None;
255    let _: Option<StaticAuthProvider> = None;
256    let _: Option<EnvSecrets> = None;
257    let _: Option<SecretValue> = None;
258    let _: Option<SecretsProviderChain> = None;
259    let _: Option<StaticResourceHandler> = None;
260    let _: Option<StaticPromptHandler> = None;
261    let _: Option<ServerConfig> = None;
262    let _: Option<ConfigValidationError> = None;
263    // Plan 05 (TKIT-07): synthesize_from_config is fn-typed; reference the
264    // function pointer to assert the re-exported path resolves at the crate root.
265    let _: fn(&ServerConfig) -> Result<Vec<crate::tools::SynthesizedTool>> = synthesize_from_config;
266    // Plan 07 (TKIT-10): SqlConnector trait stub + Dialect enum re-exports.
267    let _: Option<Dialect> = None;
268    let _: Option<ConnectorError> = None;
269    let _: Option<&dyn SqlConnector> = None;
270    // Plan 08 (TKIT-08): ServerBuilderExt trait — the headline Shape C
271    // surface. The trait is `Sized` (can't be `dyn`) — reference its method
272    // pointer instead to assert the crate-root path resolves.
273    let _: fn(pmcp::ServerBuilder, &ServerConfig) -> Result<pmcp::ServerBuilder> =
274        <pmcp::ServerBuilder as ServerBuilderExt>::try_tools_from_config;
275};
276
277// Plan 06 (TKIT-06 + TKIT-09): compile-only assertion that the code_mode
278// submodule's re-exports + wiring helpers resolve at `code_mode::*`. Gated on
279// `code-mode` because the module itself is feature-gated (D-15 + D-16: the
280// headline submodule, not a flattened crate-root surface).
281#[cfg(feature = "code-mode")]
282#[allow(dead_code)]
283const _CODE_MODE_REEXPORT_SMOKE: fn() = || {
284    let _: Option<Box<dyn crate::code_mode::CodeExecutor>> = None;
285    let _: Option<crate::code_mode::ValidationPipeline> = None;
286    let _: Option<crate::code_mode::TokenSecret> = None;
287    let _: Option<crate::code_mode::HmacTokenGenerator> = None;
288    let _: Option<crate::code_mode::ApprovalToken> = None;
289    let _: Option<crate::code_mode::NoopPolicyEvaluator> = None;
290    let _: fn(&ServerConfig) -> Result<crate::code_mode::ValidationPipeline> =
291        crate::code_mode::validation_pipeline_from_config;
292    // Plan 07 (TKIT-10 / D-12): assemble_code_mode_prompt re-exports at crate
293    // root under the code-mode feature. The fn returns a `BoxFuture`-ish async
294    // surface; reference the function pointer to assert the path resolves.
295    let _ = assemble_code_mode_prompt;
296};
297
298// Phase 92 (WBSV-01/08/09 / D-11): compile-only assertion that the FULL workbook
299// boot surface resolves at the crate root — the binding witness that a Shape A/B
300// consumer registers a governed workbook WITHOUT naming `pmcp-workbook-runtime`.
301// Gated on `workbook` because the module is feature-gated.
302#[cfg(feature = "workbook")]
303#[allow(dead_code)]
304const _WORKBOOK_REEXPORT_SMOKE: fn() = || {
305    // BundleSource (trait) + its on-disk impl + both error types — the loader
306    // inputs/outputs consumers need from the crate root.
307    let _: Option<&dyn BundleSource> = None;
308    let _: Option<LocalDirSource> = None;
309    let _: Option<BundleSourceError> = None;
310    let _: Option<BundleLoadError> = None;
311    // load_bundle (the fail-closed boot entry point) is fn-typed; reference its
312    // function pointer to assert the re-exported path resolves at the crate root.
313    let _: fn(
314        &dyn BundleSource,
315    ) -> std::result::Result<crate::workbook::WorkbookBundle, BundleLoadError> = load_bundle;
316    // WorkbookBuilderExt (the headline one-call registration) is `Sized` (can't
317    // be `dyn`) — reference its `try_` method pointer instead.
318    let _: fn(pmcp::ServerBuilder, &dyn BundleSource) -> Result<pmcp::ServerBuilder> =
319        <pmcp::ServerBuilder as WorkbookBuilderExt>::try_with_workbook_bundle;
320};
321
322// Phase 92 (WBSV-09): the embedded-source re-export resolves at the crate root
323// when the `workbook-embedded` feature layers include_dir support on top.
324#[cfg(feature = "workbook-embedded")]
325#[allow(dead_code)]
326const _WORKBOOK_EMBEDDED_REEXPORT_SMOKE: fn() = || {
327    let _: Option<EmbeddedSource> = None;
328};
329
330#[cfg(test)]
331mod demo_db_path_tests {
332    use super::demo_db_path;
333    use std::path::PathBuf;
334
335    // Why: these tests mutate the process-global LAMBDA_TASK_ROOT env var. The
336    // project runs `cargo test -- --test-threads=1` (CLAUDE.md), so they execute
337    // serially and cannot race. Each test restores the prior state.
338    #[test]
339    fn returns_tmp_path_under_lambda() {
340        let prev = std::env::var("LAMBDA_TASK_ROOT").ok();
341        std::env::set_var("LAMBDA_TASK_ROOT", "/var/task");
342        assert_eq!(demo_db_path(), PathBuf::from("/tmp/demo.db"));
343        match prev {
344            Some(v) => std::env::set_var("LAMBDA_TASK_ROOT", v),
345            None => std::env::remove_var("LAMBDA_TASK_ROOT"),
346        }
347    }
348
349    #[test]
350    fn returns_relative_path_locally() {
351        let prev = std::env::var("LAMBDA_TASK_ROOT").ok();
352        std::env::remove_var("LAMBDA_TASK_ROOT");
353        assert_eq!(demo_db_path(), PathBuf::from("demo.db"));
354        if let Some(v) = prev {
355            std::env::set_var("LAMBDA_TASK_ROOT", v);
356        }
357    }
358}