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