Release secrets-mcp 0.3.0: folder/type schema and MCP folder disambiguation
- Rename namespace/kind to folder/type on entries, audit_log, and history tables; add notes. Unique key is (user_id, folder, name). - Service layer and MCP tools support name-first lookup with optional folder when multiple entries share the same name. - secrets_delete dry_run uses the same disambiguation as real deletes. - Add scripts/migrate-v0.3.0.sql for manual DB migration. Refresh README and AGENTS.md. Made-with: Cursor
This commit is contained in:
@@ -155,17 +155,18 @@ impl SecretsService {
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct SearchInput {
|
||||
#[schemars(description = "Namespace filter (e.g. 'refining', 'ricnsmart')")]
|
||||
namespace: Option<String>,
|
||||
#[schemars(description = "Kind filter (e.g. 'server', 'service', 'key')")]
|
||||
kind: Option<String>,
|
||||
#[schemars(description = "Exact record name")]
|
||||
#[schemars(description = "Fuzzy search across name, folder, type, notes, tags, metadata")]
|
||||
query: Option<String>,
|
||||
#[schemars(description = "Folder filter (e.g. 'refining', 'personal', 'family')")]
|
||||
folder: Option<String>,
|
||||
#[schemars(description = "Type filter (e.g. 'server', 'service', 'person', 'key')")]
|
||||
#[serde(rename = "type")]
|
||||
entry_type: Option<String>,
|
||||
#[schemars(description = "Exact name to match")]
|
||||
name: Option<String>,
|
||||
#[schemars(description = "Tag filters (all must match)")]
|
||||
tags: Option<Vec<String>>,
|
||||
#[schemars(description = "Fuzzy search across name, namespace, kind, tags, metadata")]
|
||||
query: Option<String>,
|
||||
#[schemars(description = "Return only summary fields (name/tags/desc/updated_at)")]
|
||||
#[schemars(description = "Return only summary fields (name/tags/notes/updated_at)")]
|
||||
summary: Option<bool>,
|
||||
#[schemars(description = "Sort order: 'name' (default), 'updated', 'created'")]
|
||||
sort: Option<String>,
|
||||
@@ -177,24 +178,29 @@ struct SearchInput {
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct GetSecretInput {
|
||||
#[schemars(description = "Namespace of the entry")]
|
||||
namespace: String,
|
||||
#[schemars(description = "Kind of the entry (e.g. 'server', 'service')")]
|
||||
kind: String,
|
||||
#[schemars(description = "Name of the entry")]
|
||||
name: String,
|
||||
#[schemars(
|
||||
description = "Folder for disambiguation when multiple entries share the same name (optional)"
|
||||
)]
|
||||
folder: Option<String>,
|
||||
#[schemars(description = "Specific field to retrieve. If omitted, returns all fields.")]
|
||||
field: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct AddInput {
|
||||
#[schemars(description = "Namespace")]
|
||||
namespace: String,
|
||||
#[schemars(description = "Kind (e.g. 'server', 'service', 'key')")]
|
||||
kind: String,
|
||||
#[schemars(description = "Unique name within namespace+kind")]
|
||||
#[schemars(description = "Unique name for this entry")]
|
||||
name: String,
|
||||
#[schemars(description = "Folder for organization (optional, e.g. 'personal', 'refining')")]
|
||||
folder: Option<String>,
|
||||
#[schemars(
|
||||
description = "Type/category of this entry (optional, e.g. 'server', 'person', 'key')"
|
||||
)]
|
||||
#[serde(rename = "type")]
|
||||
entry_type: Option<String>,
|
||||
#[schemars(description = "Free-text notes for this entry (optional)")]
|
||||
notes: Option<String>,
|
||||
#[schemars(description = "Tags for this entry")]
|
||||
tags: Option<Vec<String>>,
|
||||
#[schemars(description = "Metadata fields as 'key=value' or 'key:=json' strings")]
|
||||
@@ -205,12 +211,14 @@ struct AddInput {
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct UpdateInput {
|
||||
#[schemars(description = "Namespace")]
|
||||
namespace: String,
|
||||
#[schemars(description = "Kind")]
|
||||
kind: String,
|
||||
#[schemars(description = "Name")]
|
||||
#[schemars(description = "Name of the entry to update")]
|
||||
name: String,
|
||||
#[schemars(
|
||||
description = "Folder for disambiguation when multiple entries share the same name (optional)"
|
||||
)]
|
||||
folder: Option<String>,
|
||||
#[schemars(description = "Update the notes field")]
|
||||
notes: Option<String>,
|
||||
#[schemars(description = "Tags to add")]
|
||||
add_tags: Option<Vec<String>>,
|
||||
#[schemars(description = "Tags to remove")]
|
||||
@@ -227,46 +235,49 @@ struct UpdateInput {
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct DeleteInput {
|
||||
#[schemars(description = "Namespace")]
|
||||
namespace: String,
|
||||
#[schemars(description = "Kind filter (required for single delete)")]
|
||||
kind: Option<String>,
|
||||
#[schemars(description = "Exact name to delete. Omit for bulk delete by namespace+kind.")]
|
||||
#[schemars(description = "Name of the entry to delete (single delete). \
|
||||
Omit to bulk delete by folder/type filters.")]
|
||||
name: Option<String>,
|
||||
#[schemars(description = "Folder filter for bulk delete")]
|
||||
folder: Option<String>,
|
||||
#[schemars(description = "Type filter for bulk delete")]
|
||||
#[serde(rename = "type")]
|
||||
entry_type: Option<String>,
|
||||
#[schemars(description = "Preview deletions without writing")]
|
||||
dry_run: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct HistoryInput {
|
||||
#[schemars(description = "Namespace")]
|
||||
namespace: String,
|
||||
#[schemars(description = "Kind")]
|
||||
kind: String,
|
||||
#[schemars(description = "Name")]
|
||||
#[schemars(description = "Name of the entry")]
|
||||
name: String,
|
||||
#[schemars(
|
||||
description = "Folder for disambiguation when multiple entries share the same name (optional)"
|
||||
)]
|
||||
folder: Option<String>,
|
||||
#[schemars(description = "Max history entries to return (default 20)")]
|
||||
limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct RollbackInput {
|
||||
#[schemars(description = "Namespace")]
|
||||
namespace: String,
|
||||
#[schemars(description = "Kind")]
|
||||
kind: String,
|
||||
#[schemars(description = "Name")]
|
||||
#[schemars(description = "Name of the entry")]
|
||||
name: String,
|
||||
#[schemars(
|
||||
description = "Folder for disambiguation when multiple entries share the same name (optional)"
|
||||
)]
|
||||
folder: Option<String>,
|
||||
#[schemars(description = "Target version number. Omit to restore the most recent snapshot.")]
|
||||
to_version: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct ExportInput {
|
||||
#[schemars(description = "Namespace filter")]
|
||||
namespace: Option<String>,
|
||||
#[schemars(description = "Kind filter")]
|
||||
kind: Option<String>,
|
||||
#[schemars(description = "Folder filter")]
|
||||
folder: Option<String>,
|
||||
#[schemars(description = "Type filter")]
|
||||
#[serde(rename = "type")]
|
||||
entry_type: Option<String>,
|
||||
#[schemars(description = "Exact name filter")]
|
||||
name: Option<String>,
|
||||
#[schemars(description = "Tag filters")]
|
||||
@@ -279,10 +290,11 @@ struct ExportInput {
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct EnvMapInput {
|
||||
#[schemars(description = "Namespace filter")]
|
||||
namespace: Option<String>,
|
||||
#[schemars(description = "Kind filter")]
|
||||
kind: Option<String>,
|
||||
#[schemars(description = "Folder filter")]
|
||||
folder: Option<String>,
|
||||
#[schemars(description = "Type filter")]
|
||||
#[serde(rename = "type")]
|
||||
entry_type: Option<String>,
|
||||
#[schemars(description = "Exact name filter")]
|
||||
name: Option<String>,
|
||||
#[schemars(description = "Tag filters")]
|
||||
@@ -316,8 +328,8 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_search",
|
||||
?user_id,
|
||||
namespace = input.namespace.as_deref(),
|
||||
kind = input.kind.as_deref(),
|
||||
folder = input.folder.as_deref(),
|
||||
entry_type = input.entry_type.as_deref(),
|
||||
name = input.name.as_deref(),
|
||||
query = input.query.as_deref(),
|
||||
"tool call start",
|
||||
@@ -326,8 +338,8 @@ impl SecretsService {
|
||||
let result = svc_search(
|
||||
&self.pool,
|
||||
SearchParams {
|
||||
namespace: input.namespace.as_deref(),
|
||||
kind: input.kind.as_deref(),
|
||||
folder: input.folder.as_deref(),
|
||||
entry_type: input.entry_type.as_deref(),
|
||||
name: input.name.as_deref(),
|
||||
tags: &tags,
|
||||
query: input.query.as_deref(),
|
||||
@@ -347,12 +359,11 @@ impl SecretsService {
|
||||
.map(|e| {
|
||||
if summary {
|
||||
serde_json::json!({
|
||||
"namespace": e.namespace,
|
||||
"kind": e.kind,
|
||||
"name": e.name,
|
||||
"folder": e.folder,
|
||||
"type": e.entry_type,
|
||||
"tags": e.tags,
|
||||
"desc": e.metadata.get("desc").or_else(|| e.metadata.get("url"))
|
||||
.and_then(|v| v.as_str()).unwrap_or(""),
|
||||
"notes": e.notes,
|
||||
"updated_at": e.updated_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
|
||||
})
|
||||
} else {
|
||||
@@ -363,9 +374,10 @@ impl SecretsService {
|
||||
.unwrap_or_default();
|
||||
serde_json::json!({
|
||||
"id": e.id,
|
||||
"namespace": e.namespace,
|
||||
"kind": e.kind,
|
||||
"name": e.name,
|
||||
"folder": e.folder,
|
||||
"type": e.entry_type,
|
||||
"notes": e.notes,
|
||||
"tags": e.tags,
|
||||
"metadata": e.metadata,
|
||||
"secret_fields": schema,
|
||||
@@ -408,8 +420,6 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_get",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
kind = %input.kind,
|
||||
name = %input.name,
|
||||
field = input.field.as_deref(),
|
||||
"tool call start",
|
||||
@@ -418,9 +428,8 @@ impl SecretsService {
|
||||
if let Some(field_name) = &input.field {
|
||||
let value = get_secret_field(
|
||||
&self.pool,
|
||||
&input.namespace,
|
||||
&input.kind,
|
||||
&input.name,
|
||||
input.folder.as_deref(),
|
||||
field_name,
|
||||
&user_key,
|
||||
Some(user_id),
|
||||
@@ -440,9 +449,8 @@ impl SecretsService {
|
||||
} else {
|
||||
let secrets = get_all_secrets(
|
||||
&self.pool,
|
||||
&input.namespace,
|
||||
&input.kind,
|
||||
&input.name,
|
||||
input.folder.as_deref(),
|
||||
&user_key,
|
||||
Some(user_id),
|
||||
)
|
||||
@@ -478,22 +486,26 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_add",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
kind = %input.kind,
|
||||
name = %input.name,
|
||||
folder = input.folder.as_deref(),
|
||||
entry_type = input.entry_type.as_deref(),
|
||||
"tool call start",
|
||||
);
|
||||
|
||||
let tags = input.tags.unwrap_or_default();
|
||||
let meta = input.meta.unwrap_or_default();
|
||||
let secrets = input.secrets.unwrap_or_default();
|
||||
let folder = input.folder.as_deref().unwrap_or("");
|
||||
let entry_type = input.entry_type.as_deref().unwrap_or("");
|
||||
let notes = input.notes.as_deref().unwrap_or("");
|
||||
|
||||
let result = svc_add(
|
||||
&self.pool,
|
||||
AddParams {
|
||||
namespace: &input.namespace,
|
||||
kind: &input.kind,
|
||||
name: &input.name,
|
||||
folder,
|
||||
entry_type,
|
||||
notes,
|
||||
tags: &tags,
|
||||
meta_entries: &meta,
|
||||
secret_entries: &secrets,
|
||||
@@ -507,8 +519,6 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_add",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
kind = %input.kind,
|
||||
name = %input.name,
|
||||
elapsed_ms = t.elapsed().as_millis(),
|
||||
"tool call ok",
|
||||
@@ -532,8 +542,6 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_update",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
kind = %input.kind,
|
||||
name = %input.name,
|
||||
"tool call start",
|
||||
);
|
||||
@@ -548,9 +556,9 @@ impl SecretsService {
|
||||
let result = svc_update(
|
||||
&self.pool,
|
||||
UpdateParams {
|
||||
namespace: &input.namespace,
|
||||
kind: &input.kind,
|
||||
name: &input.name,
|
||||
folder: input.folder.as_deref(),
|
||||
notes: input.notes.as_deref(),
|
||||
add_tags: &add_tags,
|
||||
remove_tags: &remove_tags,
|
||||
meta_entries: &meta,
|
||||
@@ -567,8 +575,6 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_update",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
kind = %input.kind,
|
||||
name = %input.name,
|
||||
elapsed_ms = t.elapsed().as_millis(),
|
||||
"tool call ok",
|
||||
@@ -578,8 +584,8 @@ impl SecretsService {
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Delete one entry (specify namespace+kind+name) or bulk delete all \
|
||||
entries matching namespace+kind. Use dry_run=true to preview.",
|
||||
description = "Delete one entry by name, or bulk delete entries matching folder and/or type. \
|
||||
Use dry_run=true to preview.",
|
||||
annotations(title = "Delete Secret Entry", destructive_hint = true)
|
||||
)]
|
||||
async fn secrets_delete(
|
||||
@@ -592,9 +598,9 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_delete",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
kind = input.kind.as_deref(),
|
||||
name = input.name.as_deref(),
|
||||
folder = input.folder.as_deref(),
|
||||
entry_type = input.entry_type.as_deref(),
|
||||
dry_run = input.dry_run.unwrap_or(false),
|
||||
"tool call start",
|
||||
);
|
||||
@@ -602,9 +608,9 @@ impl SecretsService {
|
||||
let result = svc_delete(
|
||||
&self.pool,
|
||||
DeleteParams {
|
||||
namespace: &input.namespace,
|
||||
kind: input.kind.as_deref(),
|
||||
name: input.name.as_deref(),
|
||||
folder: input.folder.as_deref(),
|
||||
entry_type: input.entry_type.as_deref(),
|
||||
dry_run: input.dry_run.unwrap_or(false),
|
||||
user_id,
|
||||
},
|
||||
@@ -615,7 +621,6 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_delete",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
elapsed_ms = t.elapsed().as_millis(),
|
||||
"tool call ok",
|
||||
);
|
||||
@@ -642,17 +647,14 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_history",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
kind = %input.kind,
|
||||
name = %input.name,
|
||||
"tool call start",
|
||||
);
|
||||
|
||||
let result = svc_history(
|
||||
&self.pool,
|
||||
&input.namespace,
|
||||
&input.kind,
|
||||
&input.name,
|
||||
input.folder.as_deref(),
|
||||
input.limit.unwrap_or(20),
|
||||
user_id,
|
||||
)
|
||||
@@ -684,8 +686,6 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_rollback",
|
||||
?user_id,
|
||||
namespace = %input.namespace,
|
||||
kind = %input.kind,
|
||||
name = %input.name,
|
||||
to_version = input.to_version,
|
||||
"tool call start",
|
||||
@@ -693,9 +693,8 @@ impl SecretsService {
|
||||
|
||||
let result = svc_rollback(
|
||||
&self.pool,
|
||||
&input.namespace,
|
||||
&input.kind,
|
||||
&input.name,
|
||||
input.folder.as_deref(),
|
||||
input.to_version,
|
||||
&user_key,
|
||||
Some(user_id),
|
||||
@@ -734,8 +733,8 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_export",
|
||||
?user_id,
|
||||
namespace = input.namespace.as_deref(),
|
||||
kind = input.kind.as_deref(),
|
||||
folder = input.folder.as_deref(),
|
||||
entry_type = input.entry_type.as_deref(),
|
||||
format,
|
||||
"tool call start",
|
||||
);
|
||||
@@ -743,8 +742,8 @@ impl SecretsService {
|
||||
let data = svc_export(
|
||||
&self.pool,
|
||||
ExportParams {
|
||||
namespace: input.namespace.as_deref(),
|
||||
kind: input.kind.as_deref(),
|
||||
folder: input.folder.as_deref(),
|
||||
entry_type: input.entry_type.as_deref(),
|
||||
name: input.name.as_deref(),
|
||||
tags: &tags,
|
||||
query: input.query.as_deref(),
|
||||
@@ -800,16 +799,16 @@ impl SecretsService {
|
||||
tracing::info!(
|
||||
tool = "secrets_env_map",
|
||||
?user_id,
|
||||
namespace = input.namespace.as_deref(),
|
||||
kind = input.kind.as_deref(),
|
||||
folder = input.folder.as_deref(),
|
||||
entry_type = input.entry_type.as_deref(),
|
||||
prefix = input.prefix.as_deref().unwrap_or(""),
|
||||
"tool call start",
|
||||
);
|
||||
|
||||
let env_map = secrets_core::service::env_map::build_env_map(
|
||||
&self.pool,
|
||||
input.namespace.as_deref(),
|
||||
input.kind.as_deref(),
|
||||
input.folder.as_deref(),
|
||||
input.entry_type.as_deref(),
|
||||
input.name.as_deref(),
|
||||
&tags,
|
||||
&only_fields,
|
||||
|
||||
Reference in New Issue
Block a user