Skip to content

Commit 81e82ab

Browse files
expose read only tools (#1817)
* expose read only tools * segregate read only tools for external use
1 parent f44c3af commit 81e82ab

4 files changed

Lines changed: 110 additions & 53 deletions

File tree

‎docs/tool-api.md‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ matching `If-None-Match` receives `304 Not Modified`.
3939

4040
The catalog is edition-specific:
4141

42-
- OSS returns OSS-supported tools.
43-
- Enterprise returns OSS tools plus Enterprise tools.
42+
- OSS returns externally callable OSS read tools.
43+
- Enterprise returns OSS read tools plus Enterprise read tools.
4444
- Parseable Cloud uses the Enterprise server catalog and exposes the tools
4545
enabled by that deployment.
4646

@@ -60,9 +60,10 @@ Request:
6060
}
6161
```
6262

63-
`arguments` must satisfy the tool's `inputSchema`. For mutation and
64-
external-side-effect tools, the MCP host or another trusted caller is
65-
responsible for applying its confirmation policy before calling this endpoint.
63+
`arguments` must satisfy the tool's `inputSchema`. The external registry
64+
currently exposes only read-only and expensive-read tools. Mutation and
65+
external-side-effect tools remain unavailable until Parseable supports a
66+
server-verifiable confirmation flow bound to the session, tool, and arguments.
6667

6768
Successful and failed executions use the MCP `CallToolResult` shape:
6869

‎src/handlers/http/modal/server.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,11 @@ impl Server {
210210
),
211211
)
212212
.service(
213-
web::resource("/tools/call")
214-
.route(web::post().to(crate::tool_executor::call_tool_registry)),
213+
web::resource("/tools/call").route(
214+
web::post()
215+
.to(crate::tool_executor::call_tool_registry)
216+
.authorize(Action::Query),
217+
),
215218
)
216219
}
217220

‎src/tool_catalog.rs‎

Lines changed: 97 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,9 @@ fn target_schema() -> Value {
302302
)
303303
}
304304

305-
/// Tools implemented by both Parseable OSS and Enterprise.
306-
///
307-
/// Enterprise may extend this catalog, but an OSS server only advertises this
308-
/// list so MCP clients never discover enterprise-only capabilities.
309-
pub fn oss_tool_specs() -> Vec<ToolSpec> {
305+
/// Read-only tools implemented by Parseable OSS and safe for generic external
306+
/// invocation without a confirmation flow.
307+
pub fn oss_read_tool_specs() -> Vec<ToolSpec> {
310308
vec![
311309
ToolSpec::new(
312310
"list_datasets",
@@ -435,33 +433,6 @@ pub fn oss_tool_specs() -> Vec<ToolSpec> {
435433
"List tags used across alerts.",
436434
empty_schema(),
437435
),
438-
ToolSpec::new(
439-
"enable_alert",
440-
"Enable alert",
441-
"Enable a disabled alert.",
442-
alert_schema(),
443-
),
444-
ToolSpec::new(
445-
"disable_alert",
446-
"Disable alert",
447-
"Disable an active alert.",
448-
alert_schema(),
449-
),
450-
ToolSpec::new(
451-
"evaluate_alert",
452-
"Evaluate alert now",
453-
"Evaluate an alert immediately. This may trigger notifications.",
454-
alert_schema(),
455-
),
456-
ToolSpec::new(
457-
"create_alert",
458-
"Create alert",
459-
"Create an alert from a complete Parseable alert specification.",
460-
object_schema(
461-
json!({ "spec": { "type": "object", "additionalProperties": true } }),
462-
&["spec"],
463-
),
464-
),
465436
ToolSpec::new(
466437
"list_alert_targets",
467438
"List alert targets",
@@ -474,15 +445,6 @@ pub fn oss_tool_specs() -> Vec<ToolSpec> {
474445
"Get the configuration for one alert target.",
475446
target_schema(),
476447
),
477-
ToolSpec::new(
478-
"create_alert_target",
479-
"Create alert target",
480-
"Create an alert notification target.",
481-
object_schema(
482-
json!({ "spec": { "type": "object", "additionalProperties": true } }),
483-
&["spec"],
484-
),
485-
),
486448
ToolSpec::new(
487449
"get_traces",
488450
"Get traces",
@@ -639,6 +601,50 @@ pub fn oss_tool_specs() -> Vec<ToolSpec> {
639601
&[],
640602
),
641603
),
604+
]
605+
}
606+
607+
/// Effectful OSS tools retained in the canonical registry for internal agents.
608+
/// Generic external invocation must not use this catalog until confirmation is
609+
/// server-verifiable.
610+
pub fn oss_action_tool_specs() -> Vec<ToolSpec> {
611+
vec![
612+
ToolSpec::new(
613+
"enable_alert",
614+
"Enable alert",
615+
"Enable a disabled alert.",
616+
alert_schema(),
617+
),
618+
ToolSpec::new(
619+
"disable_alert",
620+
"Disable alert",
621+
"Disable an active alert.",
622+
alert_schema(),
623+
),
624+
ToolSpec::new(
625+
"evaluate_alert",
626+
"Evaluate alert now",
627+
"Evaluate an alert immediately. This may trigger notifications.",
628+
alert_schema(),
629+
),
630+
ToolSpec::new(
631+
"create_alert",
632+
"Create alert",
633+
"Create an alert from a complete Parseable alert specification.",
634+
object_schema(
635+
json!({ "spec": { "type": "object", "additionalProperties": true } }),
636+
&["spec"],
637+
),
638+
),
639+
ToolSpec::new(
640+
"create_alert_target",
641+
"Create alert target",
642+
"Create an alert notification target.",
643+
object_schema(
644+
json!({ "spec": { "type": "object", "additionalProperties": true } }),
645+
&["spec"],
646+
),
647+
),
642648
ToolSpec::new(
643649
"update_alert",
644650
"Update alert",
@@ -745,10 +751,21 @@ pub fn oss_tool_specs() -> Vec<ToolSpec> {
745751
]
746752
}
747753

754+
/// Canonical OSS catalog used by internal agents and imported by Enterprise.
755+
pub fn oss_tool_specs() -> Vec<ToolSpec> {
756+
let mut tools = oss_read_tool_specs();
757+
tools.extend(oss_action_tool_specs());
758+
tools
759+
}
760+
748761
pub fn is_oss_tool(name: &str) -> bool {
749762
oss_tool_specs().iter().any(|tool| tool.name == name)
750763
}
751764

765+
pub fn is_oss_read_tool(name: &str) -> bool {
766+
oss_read_tool_specs().iter().any(|tool| tool.name == name)
767+
}
768+
752769
pub fn registry_http_response(request: &HttpRequest, tools: Vec<Value>) -> HttpResponse {
753770
let payload = json!({ "tools": tools });
754771
let body = serde_json::to_string(&payload).expect("tool registry is JSON serializable");
@@ -775,7 +792,7 @@ pub fn registry_http_response(request: &HttpRequest, tools: Vec<Value>) -> HttpR
775792

776793
/// `GET /api/prism/v1/llm/tools/list`
777794
pub async fn list_tool_registry(request: HttpRequest) -> HttpResponse {
778-
let tools = oss_tool_specs()
795+
let tools = oss_read_tool_specs()
779796
.into_iter()
780797
.map(|tool| {
781798
json!({
@@ -797,7 +814,10 @@ mod tests {
797814
use actix_web::{App, http::header, test, web};
798815
use serde_json::Value;
799816

800-
use super::{ToolCallRequest, ToolCallResult, list_tool_registry, oss_tool_specs};
817+
use super::{
818+
ToolCallRequest, ToolCallResult, ToolEffect, is_oss_read_tool, list_tool_registry,
819+
oss_action_tool_specs, oss_read_tool_specs, oss_tool_specs,
820+
};
801821

802822
#[actix_web::test]
803823
async fn oss_catalog_has_unique_names() {
@@ -819,9 +839,42 @@ mod tests {
819839
)
820840
.await;
821841

822-
assert_eq!(response["tools"].as_array().unwrap().len(), 47);
842+
assert_eq!(
843+
response["tools"].as_array().unwrap().len(),
844+
oss_read_tool_specs().len()
845+
);
823846
assert_eq!(response["tools"][0]["name"], "list_datasets");
824847
assert_eq!(response["tools"][0]["inputSchema"]["type"], "object");
848+
assert!(oss_read_tool_specs().iter().all(|tool| matches!(
849+
tool.effect(),
850+
ToolEffect::ReadOnly | ToolEffect::ExpensiveRead
851+
)));
852+
assert!(
853+
response["tools"]
854+
.as_array()
855+
.unwrap()
856+
.iter()
857+
.all(|tool| tool["name"] != "create_alert")
858+
);
859+
}
860+
861+
#[actix_web::test]
862+
async fn read_and_action_catalogs_are_structurally_separated() {
863+
let read_tools = oss_read_tool_specs();
864+
let action_tools = oss_action_tool_specs();
865+
866+
assert_eq!(read_tools.len(), 34);
867+
assert_eq!(action_tools.len(), 13);
868+
assert!(read_tools.iter().all(|tool| matches!(
869+
tool.effect(),
870+
ToolEffect::ReadOnly | ToolEffect::ExpensiveRead
871+
)));
872+
assert!(action_tools.iter().all(|tool| matches!(
873+
tool.effect(),
874+
ToolEffect::Mutation | ToolEffect::ExternalSideEffect
875+
)));
876+
assert!(read_tools.iter().all(|tool| is_oss_read_tool(tool.name)));
877+
assert!(action_tools.iter().all(|tool| !is_oss_read_tool(tool.name)));
825878
}
826879

827880
#[actix_web::test]

‎src/tool_executor.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
parseable::PARSEABLE,
1919
prism::logstream::get_prism_logstream_info,
2020
rbac::{Response, Users, map::SessionKey, role::Action},
21-
tool_catalog::{ToolCallRequest, ToolCallResult, oss_tool_specs},
21+
tool_catalog::{ToolCallRequest, ToolCallResult, oss_read_tool_specs},
2222
utils::{actix::extract_session_key_from_req, get_tenant_id_from_request, time::TimeRange},
2323
};
2424

@@ -897,7 +897,7 @@ pub async fn call_tool_registry(
897897
"Your session has expired or is no longer valid. Please re-authenticate to access this resource.",
898898
));
899899
}
900-
let Some(spec) = oss_tool_specs()
900+
let Some(spec) = oss_read_tool_specs()
901901
.into_iter()
902902
.find(|tool| tool.name == call.name)
903903
else {

0 commit comments

Comments
 (0)