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