From b01a5fd758b98d2cfef3dd363819be1f4f85e03a Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 9 Oct 2026 09:49:53 +0300 Subject: [PATCH 01/12] feat(tinymcp): tag MCP tools with server and remote name McpServerTool now reports tags identifying its server family, server id and remote tool name, so host tool rules can target a server's tools by the names the server itself uses rather than the slugged registered name. The tinytools dependency is bumped to the revision that provides the tags hook. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.toml | 2 +- crates/tinymcp/src/tools/tool.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e9ed68e..35ce830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ schemars = { version = "1", default-features = false, features = ["derive"] } # to one package and `tinytools::ToolResult` stays one type. A second path # dependency could not be unified that way. Pinned to the revision tinyagents # vendors, which is the copy OpenHuman links. -tinytools = { git = "https://github.com/tinyhumansai/tinytools", rev = "82c0d975e2667e1c525afe525006134431641785" } +tinytools = { git = "https://github.com/tinyhumansai/tinytools", rev = "59e1cd3bbdc96ad54c8647537601b7eb6ed5a6f4" } # `#[async_trait]` on the tool adapter and the invoker trait, matching how # `tinytools::Tool` itself is declared. async-trait = "0.1" diff --git a/crates/tinymcp/src/tools/tool.rs b/crates/tinymcp/src/tools/tool.rs index b244b3b..e6ac1a0 100644 --- a/crates/tinymcp/src/tools/tool.rs +++ b/crates/tinymcp/src/tools/tool.rs @@ -141,6 +141,18 @@ impl Tool for McpServerTool { Some(&self.family) } + /// `mcp.server:`, `mcp.server_id:` and `mcp.tool:`, so a host's tool rules can target one server's tools by the + /// names the server itself uses: the registered name is a slug with a + /// digest suffix that a pattern cannot reliably split. + fn tags(&self) -> Vec { + vec![ + format!("mcp.server:{}", self.family), + format!("mcp.server_id:{}", self.server_id), + format!("mcp.tool:{}", self.remote_name), + ] + } + async fn execute(&self, arguments: Value) -> anyhow::Result { // MCP requires an object. Some providers JSON-encode it, or send // nothing; read it the way every other call path does, and answer a From bf35d7bf3f0633848dbc62ac9e939a421a6036d3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 9 Oct 2026 09:50:43 +0300 Subject: [PATCH 02/12] test(tinymcp): cover tool tags used by rule matching Add a test asserting that tools are tagged with their server and remote name, and that a tag-matching deny rule blocks the call. The tinytools dependency is bumped to 0.5.0, which provides the tag-based rule matching this test exercises. Auto-committed-on: dragonfly Co-authored-by: Medulla --- Cargo.lock | 4 ++-- crates/tinymcp/src/tools/mod_tests.rs | 30 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51ccd0e..6125ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1756,8 +1756,8 @@ dependencies = [ [[package]] name = "tinytools" -version = "0.4.1" -source = "git+https://github.com/tinyhumansai/tinytools?rev=82c0d975e2667e1c525afe525006134431641785#82c0d975e2667e1c525afe525006134431641785" +version = "0.5.0" +source = "git+https://github.com/tinyhumansai/tinytools?rev=59e1cd3bbdc96ad54c8647537601b7eb6ed5a6f4#59e1cd3bbdc96ad54c8647537601b7eb6ed5a6f4" dependencies = [ "anyhow", "async-trait", diff --git a/crates/tinymcp/src/tools/mod_tests.rs b/crates/tinymcp/src/tools/mod_tests.rs index 513e8f4..148a87f 100644 --- a/crates/tinymcp/src/tools/mod_tests.rs +++ b/crates/tinymcp/src/tools/mod_tests.rs @@ -329,6 +329,36 @@ fn a_tool_declares_a_remote_effectful_call_grouped_by_server() { assert_eq!(tool.clone().renamed("old").name(), "old"); } +#[test] +fn a_tool_is_tagged_so_rules_can_target_its_server_and_remote_name() { + let mut source = overview("id-1", "@acme/ticktick-mcp", &[]); + source.tools.push(McpTool { + name: "deleteGoal".into(), + description: None, + input_schema: json!({}), + }); + let tools = tools_for(&[McpToolSource::from_overview(&source)], &unreachable()); + assert_eq!( + tools[0].tags(), + [ + "mcp.server:@acme/ticktick-mcp", + "mcp.server_id:id-1", + "mcp.tool:deleteGoal", + ] + ); + let rules: tinytools::ToolRules = serde_json::from_value(json!({ "rules": [ + { "effect": "deny", "match": { "tags": "mcp.tool:delete*" } }, + ] })) + .unwrap(); + let decision = rules.evaluate( + &tinytools::ToolSubject::of(tools[0].as_ref()), + &tinytools::RuleContext::new(), + tinytools::Surface::Call, + None, + ); + assert!(!decision.callable); +} + #[test] fn a_malformed_root_schema_type_is_replaced_with_object() { for invalid_type in [json!("string"), Value::Null, json!(["object", "null"])] { From 5eb29c2164c1570aad9489b6861893aa112f33ee Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 9 Oct 2026 09:51:30 +0300 Subject: [PATCH 03/12] test(tools): pass tool reference directly to subject The test now borrows the tool value instead of calling as_ref on it, matching the expected argument type and keeping the assertion unchanged. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymcp/src/tools/mod_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinymcp/src/tools/mod_tests.rs b/crates/tinymcp/src/tools/mod_tests.rs index 148a87f..971ea45 100644 --- a/crates/tinymcp/src/tools/mod_tests.rs +++ b/crates/tinymcp/src/tools/mod_tests.rs @@ -351,7 +351,7 @@ fn a_tool_is_tagged_so_rules_can_target_its_server_and_remote_name() { ] })) .unwrap(); let decision = rules.evaluate( - &tinytools::ToolSubject::of(tools[0].as_ref()), + &tinytools::ToolSubject::of(&tools[0]), &tinytools::RuleContext::new(), tinytools::Surface::Call, None, From 6c49bb8cb5d756bdaedd2ee54a814b28af952bbe Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 9 Oct 2026 09:53:13 +0300 Subject: [PATCH 04/12] docs(tools): document tool family and tag conventions Explain that each tool's family is its server label and that tags carry the server label, server id and remote tool name, so host rules can target a server's tools by the names it uses. Note that the registered name's slug and digest suffix cannot be split reliably by a pattern, and that per-server allowed and disallowed lists remain exact fetch-time filters. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinymcp/src/tools/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/tinymcp/src/tools/README.md b/crates/tinymcp/src/tools/README.md index 54994fa..50a1282 100644 --- a/crates/tinymcp/src/tools/README.md +++ b/crates/tinymcp/src/tools/README.md @@ -13,6 +13,14 @@ remote descriptions and titles, ensures the root schema declares an object, and replaces schemas over the model-facing size limit with an empty object schema. +Each tool's `family` is its server label. Its `tags` are +`mcp.server: