release: secrets-mcp 0.5.2
Some checks failed
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 6m7s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Failing after 6s

Bump version: secrets-mcp-0.5.1 tag already existed while crates had further changes.

Made-with: Cursor
This commit is contained in:
2026-04-05 10:38:50 +08:00
parent aefad33870
commit dd24f7cc44
15 changed files with 787 additions and 66 deletions

View File

@@ -18,6 +18,8 @@ use serde_json::{Map, Value};
use sqlx::PgPool;
use uuid::Uuid;
use crate::validation;
// ── Serde helpers for numeric parameters that may arrive as strings ──────────
mod deser {
@@ -593,6 +595,44 @@ fn map_to_kv_strings(map: Map<String, Value>) -> Vec<String> {
.collect()
}
/// Check if any KV string would trigger a server-side file read.
///
/// `parse_kv` in secrets-core supports two file-read syntaxes:
/// - `key=@path` (has `=`, value starts with `@`)
/// - `key@path` (no `=`, split on `@`)
///
/// Both are legitimate for CLI usage but must be rejected in the MCP context
/// where the server process runs remotely and the caller controls the path.
///
/// Note: `key:=json` is intentionally skipped here. Although the value may
/// contain `@` characters (e.g. `config:=@/etc/passwd`), the `:=` branch in
/// `parse_kv` treats the right-hand side as raw JSON and never performs file
/// reads. The `@` in such cases is just data, not a file reference.
fn contains_file_reference(entries: &[String]) -> Option<String> {
for entry in entries {
// key:=json — safe, skip before checking for `=`
if entry.contains(":=") {
continue;
}
// key=@path
if let Some((_, value)) = entry.split_once('=') {
if value.starts_with('@') {
return Some(entry.clone());
}
continue;
}
// key@path (no `=` present)
// parse_kv treats entries without `=` that contain `@` as file-read
// syntax (key@path). This includes strings like "user@example.com"
// if passed without a `=` separator — which is correct to reject here
// since the MCP server runs remotely and cannot read local files.
if entry.contains('@') {
return Some(entry.clone());
}
}
None
}
/// Parse a UUID string, returning an MCP error on failure.
fn parse_uuid(s: &str) -> Result<Uuid, rmcp::ErrorData> {
s.parse::<Uuid>()
@@ -879,10 +919,33 @@ impl SecretsService {
if let Some(obj) = input.meta_obj {
meta.extend(map_to_kv_strings(obj));
}
if let Some(offending) = contains_file_reference(&meta) {
return Err(rmcp::ErrorData::invalid_params(
format!("@file syntax is not allowed in MCP tools: '{}'", offending),
None,
));
}
let mut secrets = input.secrets.unwrap_or_default();
if let Some(obj) = input.secrets_obj {
secrets.extend(map_to_kv_strings(obj));
}
if let Some(offending) = contains_file_reference(&secrets) {
return Err(rmcp::ErrorData::invalid_params(
format!("@file syntax is not allowed in MCP tools: '{}'", offending),
None,
));
}
// Input length validation
validation::validate_input_lengths(
&input.name,
input.folder.as_deref(),
input.entry_type.as_deref(),
input.notes.as_deref(),
)?;
validation::validate_tags(&tags)?;
validation::validate_meta_entries(&meta)?;
let secret_types = input.secret_types.unwrap_or_default();
let secret_types_map: std::collections::HashMap<String, String> = secret_types
.into_iter()
@@ -962,11 +1025,34 @@ impl SecretsService {
if let Some(obj) = input.meta_obj {
meta.extend(map_to_kv_strings(obj));
}
if let Some(offending) = contains_file_reference(&meta) {
return Err(rmcp::ErrorData::invalid_params(
format!("@file syntax is not allowed in MCP tools: '{}'", offending),
None,
));
}
let remove_meta = input.remove_meta.unwrap_or_default();
let mut secrets = input.secrets.unwrap_or_default();
if let Some(obj) = input.secrets_obj {
secrets.extend(map_to_kv_strings(obj));
}
if let Some(offending) = contains_file_reference(&secrets) {
return Err(rmcp::ErrorData::invalid_params(
format!("@file syntax is not allowed in MCP tools: '{}'", offending),
None,
));
}
// Input length validation
validation::validate_input_lengths(
&input.name,
input.folder.as_deref(),
None,
input.notes.as_deref(),
)?;
validation::validate_tags(&add_tags)?;
validation::validate_meta_entries(&meta)?;
let secret_types = input.secret_types.unwrap_or_default();
let secret_types_map: std::collections::HashMap<String, String> = secret_types
.into_iter()