mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-13 03:30:46 +08:00
Remove MCP wire suppression; keep specialist binding only.
Drop Admin 线侧策略 UI/APIs and gut tool_wire_policy to schema+size prep so MCP tools are gated solely by expert bindings.
This commit is contained in:
parent
4277ad6b14
commit
fe29b43695
10 changed files with 127 additions and 2211 deletions
|
|
@ -1,137 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from interfaces.http.fastapi_app import create_app
|
||||
from svc.persistence.sqlite_store import SqliteStore
|
||||
|
||||
|
||||
class AdminMcpToolWireRoleModeApiTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
|
||||
self.db = Path(self._tmp.name) / "ops.sqlite"
|
||||
os.environ["OPS_ASSISTANT_DB_PATH"] = str(self.db)
|
||||
os.environ["OPS_ASSISTANT_PASSWORD"] = "test-admin-pass"
|
||||
self.store = SqliteStore(str(self.db))
|
||||
t = self.store.create_tenant("Team")
|
||||
self.tenant_id = str(t["id"])
|
||||
self.store.create_user_account(
|
||||
tenant_id=self.tenant_id,
|
||||
username="administrator",
|
||||
display_name="Admin",
|
||||
role="owner",
|
||||
password_hash=hashlib.sha256("test-admin-pass".encode("utf-8")).hexdigest(),
|
||||
is_active=True,
|
||||
)
|
||||
self.client = TestClient(create_app())
|
||||
self.client.post("/admin/api/auth/bootstrap", json={})
|
||||
r = self.client.post(
|
||||
"/admin/api/auth/login",
|
||||
json={"tenant_id": self.tenant_id, "username": "administrator", "password": "test-admin-pass", "purpose": "console"},
|
||||
)
|
||||
self.token = str((r.json() or {}).get("token") or "")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _h(self) -> dict[str, str]:
|
||||
return {"authorization": f"Bearer {self.token}"}
|
||||
|
||||
def test_role_mode_save_and_snapshot(self) -> None:
|
||||
s = self.client.post(
|
||||
"/admin/api/mcp/tool-wire/role-mode",
|
||||
json={"role": "ops", "mode": "forbidden"},
|
||||
headers=self._h(),
|
||||
)
|
||||
self.assertEqual(s.status_code, 200, s.text)
|
||||
body = s.json() or {}
|
||||
self.assertTrue(body.get("ok"))
|
||||
self.assertEqual(body.get("role"), "ops")
|
||||
self.assertEqual(body.get("mode"), "forbidden")
|
||||
|
||||
g = self.client.get("/admin/api/mcp/tool-wire?role=ops", headers=self._h())
|
||||
self.assertEqual(g.status_code, 200, g.text)
|
||||
gb = g.json() or {}
|
||||
self.assertTrue(gb.get("ok"))
|
||||
self.assertEqual(gb.get("role"), "ops")
|
||||
self.assertEqual(gb.get("role_mode"), "forbidden")
|
||||
|
||||
def test_role_mode_rejects_unknown_role(self) -> None:
|
||||
s = self.client.post(
|
||||
"/admin/api/mcp/tool-wire/role-mode",
|
||||
json={"role": "unknown_role_x", "mode": "restricted"},
|
||||
headers=self._h(),
|
||||
)
|
||||
self.assertEqual(s.status_code, 400, s.text)
|
||||
self.assertIn("invalid_role", s.text)
|
||||
|
||||
def test_policies_batch_supports_role_scope(self) -> None:
|
||||
r = self.client.post(
|
||||
"/admin/api/mcp/tool-wire/policies/batch",
|
||||
json={"role": "ops", "level": 9999, "wire_names": ["mcp__echo__ping"]},
|
||||
headers=self._h(),
|
||||
)
|
||||
self.assertEqual(r.status_code, 200, r.text)
|
||||
body = r.json() or {}
|
||||
self.assertTrue(body.get("ok"))
|
||||
self.assertEqual(body.get("role"), "ops")
|
||||
raw = str(self.store.get_setting("mcp_tool_wire_tool_policies_by_role") or "").strip() or "{}"
|
||||
obj = json.loads(raw)
|
||||
self.assertEqual(int(((obj.get("ops") or {}).get("mcp__echo__ping") or 0)), 9999)
|
||||
|
||||
def test_e2e_role_mode_then_batch_then_refresh_consistent(self) -> None:
|
||||
# 1) switch role mode for ops
|
||||
s = self.client.post(
|
||||
"/admin/api/mcp/tool-wire/role-mode",
|
||||
json={"role": "ops", "mode": "restricted"},
|
||||
headers=self._h(),
|
||||
)
|
||||
self.assertEqual(s.status_code, 200, s.text)
|
||||
self.assertTrue((s.json() or {}).get("ok"))
|
||||
|
||||
# 2) apply role-scoped batch policy for ops
|
||||
b = self.client.post(
|
||||
"/admin/api/mcp/tool-wire/policies/batch",
|
||||
json={"role": "ops", "level": 0, "wire_names": ["mcp__echo__ping"]},
|
||||
headers=self._h(),
|
||||
)
|
||||
self.assertEqual(b.status_code, 200, b.text)
|
||||
bb = b.json() or {}
|
||||
self.assertTrue(bb.get("ok"))
|
||||
self.assertEqual(bb.get("role"), "ops")
|
||||
|
||||
# 3) query snapshot for ops (equivalent to refresh + reload)
|
||||
g1 = self.client.get("/admin/api/mcp/tool-wire?role=ops", headers=self._h())
|
||||
self.assertEqual(g1.status_code, 200, g1.text)
|
||||
body1 = g1.json() or {}
|
||||
self.assertTrue(body1.get("ok"))
|
||||
self.assertEqual(body1.get("role"), "ops")
|
||||
self.assertEqual(body1.get("role_mode"), "restricted")
|
||||
self.assertEqual(int((body1.get("policies") or {}).get("mcp__echo__ping") or 0), 0)
|
||||
|
||||
# 4) switch another role and ensure isolation
|
||||
s2 = self.client.post(
|
||||
"/admin/api/mcp/tool-wire/role-mode",
|
||||
json={"role": "generalist", "mode": "forbidden"},
|
||||
headers=self._h(),
|
||||
)
|
||||
self.assertEqual(s2.status_code, 200, s2.text)
|
||||
|
||||
g2 = self.client.get("/admin/api/mcp/tool-wire?role=generalist", headers=self._h())
|
||||
self.assertEqual(g2.status_code, 200, g2.text)
|
||||
body2 = g2.json() or {}
|
||||
self.assertEqual(body2.get("role_mode"), "forbidden")
|
||||
# no accidental carry-over from ops role policies
|
||||
self.assertNotIn("mcp__echo__ping", dict(body2.get("policies") or {}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
|
@ -1,213 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch
|
||||
|
||||
from svc.llm.tool_wire_policy import (
|
||||
SETTINGS_KEY_PENALTY_STATE,
|
||||
migrate_legacy_penalty_store,
|
||||
prepare_openai_tools_for_llm_api,
|
||||
)
|
||||
|
||||
|
||||
class FakeToolStore:
|
||||
"""Minimal store stub for ``prepare_openai_tools_for_llm_api``."""
|
||||
|
||||
def __init__(self, usage_map: dict, penalty_state: dict | None = None) -> None:
|
||||
self.usage_map = dict(usage_map)
|
||||
self._penalty_json = json.dumps(penalty_state or {})
|
||||
self.last_penalty_saved: str | None = None
|
||||
|
||||
def list_mcp_tool_aggregate_usage(self) -> dict:
|
||||
return dict(self.usage_map)
|
||||
|
||||
def get_setting(self, key: str) -> str | None:
|
||||
if key == SETTINGS_KEY_PENALTY_STATE:
|
||||
return self._penalty_json
|
||||
return None
|
||||
|
||||
def set_setting(self, key: str, val: str) -> None:
|
||||
if key == SETTINGS_KEY_PENALTY_STATE:
|
||||
self._penalty_json = val
|
||||
self.last_penalty_saved = val
|
||||
|
||||
|
||||
def _fn(name: str, *, desc: str = "hello") -> dict:
|
||||
props: dict = {"fld": {"type": "string", "description": "inner"}}
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"parameters": {"type": "object", "properties": props},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _names(tools: list[dict]) -> list[str]:
|
||||
out: list[str] = []
|
||||
for t in tools:
|
||||
fn = t.get("function")
|
||||
if isinstance(fn, dict) and fn.get("name"):
|
||||
out.append(str(fn["name"]))
|
||||
return out
|
||||
|
||||
|
||||
def _iso_hours_ago(h: float) -> str:
|
||||
n = datetime(2026, 1, 5, 12, 0, 0, tzinfo=timezone.utc)
|
||||
return (n - timedelta(hours=h)).isoformat()
|
||||
|
||||
|
||||
class ToolWirePolicyTests(unittest.TestCase):
|
||||
def test_builtin_tools_always_full(self) -> None:
|
||||
ts = _iso_hours_ago(1.0)
|
||||
usage = {
|
||||
"mcp__rank__1": {"count": 400, "last_ts": ts},
|
||||
"mcp__rank__2": {"count": 300, "last_ts": ts},
|
||||
"mcp__rank__3": {"count": 200, "last_ts": ts},
|
||||
"mcp__srv__x": {"count": 1, "last_ts": ts},
|
||||
}
|
||||
tools = [_fn("read_file"), _fn("mcp__rank__1"), _fn("mcp__rank__2"), _fn("mcp__rank__3"), _fn("mcp__srv__x")]
|
||||
st = FakeToolStore(usage)
|
||||
with patch("svc.llm.tool_wire_policy._utc_now", return_value=datetime(2026, 1, 5, 12, 0, 0, tzinfo=timezone.utc)):
|
||||
with patch.dict("os.environ", {"OPS_MCP_WIRE_TOP_N_FULL": "3"}, clear=False):
|
||||
out = prepare_openai_tools_for_llm_api(
|
||||
tools,
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
max_json_bytes=None,
|
||||
store=st,
|
||||
)
|
||||
by_name = {str(t["function"]["name"]): t for t in out if t.get("type") == "function"}
|
||||
self.assertIn("inner", json.dumps(by_name["read_file"]))
|
||||
self.assertNotIn("inner", json.dumps(by_name["mcp__srv__x"]))
|
||||
|
||||
def test_graduated_minimal_below_top_n(self) -> None:
|
||||
ts = _iso_hours_ago(0.5)
|
||||
usage = {
|
||||
"mcp__p__a": {"count": 100, "last_ts": ts},
|
||||
"mcp__p__b": {"count": 90, "last_ts": ts},
|
||||
"mcp__p__c": {"count": 80, "last_ts": ts},
|
||||
"mcp__p__d": {"count": 70, "last_ts": ts},
|
||||
}
|
||||
tools = [_fn("mcp__p__a"), _fn("mcp__p__b"), _fn("mcp__p__c"), _fn("mcp__p__d")]
|
||||
st = FakeToolStore(usage)
|
||||
fixed = datetime(2026, 1, 5, 12, 0, 0, tzinfo=timezone.utc)
|
||||
with patch("svc.llm.tool_wire_policy._utc_now", return_value=fixed):
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"OPS_MCP_WIRE_TOP_N_FULL": "3",
|
||||
"OPS_MCP_WIRE_MEDIUM_RANK_START": "999",
|
||||
"OPS_MCP_WIRE_MEDIUM_RANK_END": "999",
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
out = prepare_openai_tools_for_llm_api(
|
||||
tools,
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
max_json_bytes=None,
|
||||
store=st,
|
||||
)
|
||||
by_name = {str(t["function"]["name"]): t for t in out}
|
||||
self.assertIn("properties", by_name["mcp__p__a"]["function"]["parameters"])
|
||||
self.assertIn("properties", by_name["mcp__p__b"]["function"]["parameters"])
|
||||
self.assertIn("properties", by_name["mcp__p__c"]["function"]["parameters"])
|
||||
self.assertEqual(by_name["mcp__p__d"]["function"]["parameters"].get("additionalProperties"), True)
|
||||
|
||||
def test_never_logged_mcp_not_omitted(self) -> None:
|
||||
tools = [_fn("mcp__ghost__z")]
|
||||
st = FakeToolStore({})
|
||||
out = prepare_openai_tools_for_llm_api(
|
||||
tools,
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
max_json_bytes=None,
|
||||
store=st,
|
||||
)
|
||||
self.assertEqual(_names(out), ["mcp__ghost__z"])
|
||||
|
||||
def test_stale_penalty_then_release(self) -> None:
|
||||
t0 = datetime(2026, 4, 19, 12, 0, 0, tzinfo=timezone.utc)
|
||||
stale_ts = (t0 - timedelta(hours=4)).isoformat()
|
||||
usage = {
|
||||
"mcp__st__fill1": {"count": 900, "last_ts": stale_ts},
|
||||
"mcp__st__fill2": {"count": 800, "last_ts": stale_ts},
|
||||
"mcp__st__fill3": {"count": 700, "last_ts": stale_ts},
|
||||
"mcp__st__old": {"count": 5, "last_ts": stale_ts},
|
||||
}
|
||||
tools = [_fn("mcp__st__fill1"), _fn("mcp__st__fill2"), _fn("mcp__st__fill3"), _fn("mcp__st__old")]
|
||||
st = FakeToolStore(usage)
|
||||
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{"OPS_MCP_WIRE_STALE_HOURS": "3", "OPS_MCP_WIRE_TOP_N_FULL": "3"},
|
||||
clear=False,
|
||||
):
|
||||
with patch("svc.llm.tool_wire_policy._utc_now", return_value=t0):
|
||||
out1 = prepare_openai_tools_for_llm_api(
|
||||
tools,
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
max_json_bytes=None,
|
||||
store=st,
|
||||
)
|
||||
self.assertEqual(_names(out1), ["mcp__st__fill1", "mcp__st__fill2", "mcp__st__fill3"])
|
||||
|
||||
pen = json.loads(st.last_penalty_saved or "{}")
|
||||
self.assertEqual(pen["mcp__st__old"]["phase"], "active")
|
||||
|
||||
t_late = t0 + timedelta(minutes=31)
|
||||
with patch("svc.llm.tool_wire_policy._utc_now", return_value=t_late):
|
||||
out2 = prepare_openai_tools_for_llm_api(
|
||||
tools,
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
max_json_bytes=None,
|
||||
store=st,
|
||||
)
|
||||
want = ["mcp__st__fill1", "mcp__st__fill2", "mcp__st__fill3", "mcp__st__old"]
|
||||
self.assertEqual(_names(out2), want)
|
||||
pen2 = json.loads(st.last_penalty_saved or "{}")
|
||||
self.assertEqual(pen2["mcp__st__old"]["phase"], "done")
|
||||
|
||||
def test_wave_ts_change_clears_penalty(self) -> None:
|
||||
t0 = datetime(2026, 4, 19, 15, 0, 0, tzinfo=timezone.utc)
|
||||
old_ts = (t0 - timedelta(hours=5)).isoformat()
|
||||
usage1 = {"mcp__w__x": {"count": 1, "last_ts": old_ts}}
|
||||
penalty1 = {"mcp__w__x": {"phase": "done", "wave_ts": old_ts}}
|
||||
tools = [_fn("mcp__w__x")]
|
||||
st = FakeToolStore(usage1, penalty1)
|
||||
|
||||
new_ts = (t0 - timedelta(minutes=30)).isoformat()
|
||||
usage2 = {"mcp__w__x": {"count": 2, "last_ts": new_ts}}
|
||||
st.usage_map = usage2
|
||||
|
||||
with patch.dict(
|
||||
"os.environ",
|
||||
{"OPS_MCP_WIRE_STALE_HOURS": "3", "OPS_MCP_WIRE_TOP_N_FULL": "3"},
|
||||
clear=False,
|
||||
):
|
||||
with patch("svc.llm.tool_wire_policy._utc_now", return_value=t0):
|
||||
out = prepare_openai_tools_for_llm_api(
|
||||
tools,
|
||||
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
max_json_bytes=None,
|
||||
store=st,
|
||||
)
|
||||
pen = json.loads(st.last_penalty_saved or "{}")
|
||||
self.assertNotIn("mcp__w__x", pen)
|
||||
self.assertEqual(_names(out), ["mcp__w__x"])
|
||||
|
||||
def test_migrate_legacy_penalty_store(self) -> None:
|
||||
future = (datetime.now(timezone.utc) + timedelta(hours=1)).isoformat()
|
||||
past = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat()
|
||||
raw = {
|
||||
"legacy_active": {"omit_until": future, "wave_ts": "x"},
|
||||
"legacy_done": {"omit_until": past, "wave_ts": "y"},
|
||||
}
|
||||
mig = migrate_legacy_penalty_store(raw)
|
||||
self.assertEqual(mig["legacy_active"]["phase"], "active")
|
||||
self.assertEqual(mig["legacy_done"]["phase"], "done")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
50
tests/test_tool_wire_policy_passthrough.py
Normal file
50
tests/test_tool_wire_policy_passthrough.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""Wire suppression removed: prepare_openai_tools keeps MCP tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from svc.llm.tool_wire_policy import (
|
||||
load_role_mode_for_role,
|
||||
prepare_openai_tools_for_llm_api,
|
||||
wire_graduation_effective,
|
||||
wire_policy_enabled,
|
||||
)
|
||||
|
||||
|
||||
def test_wire_policy_disabled_by_default() -> None:
|
||||
assert wire_policy_enabled("https://api.openai.com") is False
|
||||
assert wire_graduation_effective("https://api.openai.com", {}) is False
|
||||
assert load_role_mode_for_role(None, role="ops") == "unrestricted"
|
||||
|
||||
|
||||
def test_prepare_openai_tools_keeps_mcp_tools() -> None:
|
||||
tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "mcp__demo__ping",
|
||||
"description": "ping",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "read",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
]
|
||||
out = prepare_openai_tools_for_llm_api(
|
||||
tools,
|
||||
base_url="https://example",
|
||||
max_json_bytes=None,
|
||||
store=None,
|
||||
role="ops",
|
||||
)
|
||||
names = [
|
||||
str(((e.get("function") or {}).get("name") if isinstance(e, dict) else "") or "")
|
||||
for e in out
|
||||
]
|
||||
assert "mcp__demo__ping" in names
|
||||
assert "read_file" in names
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from svc.persistence.sqlite_store import SqliteStore
|
||||
from svc.llm.tool_wire_policy import prepare_openai_tools_for_llm_api
|
||||
|
||||
|
||||
def test_manager_role_respects_permanent_ban_by_default(tmp_path: Path) -> None:
|
||||
store = SqliteStore(str(tmp_path / "ops.sqlite"))
|
||||
store.set_setting("mcp_tool_wire_tool_policies", '{"mcp__echo__ping": 9999}')
|
||||
tools = [
|
||||
{"type": "function", "function": {"name": "mcp__echo__ping", "description": "Ping", "parameters": {"type": "object"}}},
|
||||
{"type": "function", "function": {"name": "system_time", "description": "t", "parameters": {"type": "object"}}},
|
||||
]
|
||||
out_mgr = prepare_openai_tools_for_llm_api(tools, base_url="https://example", max_json_bytes=None, store=store, role="manager")
|
||||
out_gen = prepare_openai_tools_for_llm_api(tools, base_url="https://example", max_json_bytes=None, store=store, role="generalist")
|
||||
mgr_names = {t["function"]["name"] for t in out_mgr if isinstance(t, dict) and t.get("type") == "function"}
|
||||
gen_names = {t["function"]["name"] for t in out_gen if isinstance(t, dict) and t.get("type") == "function"}
|
||||
assert "mcp__echo__ping" not in mgr_names
|
||||
assert "mcp__echo__ping" not in gen_names
|
||||
|
||||
|
||||
def test_role_scoped_policies_override_global(tmp_path: Path) -> None:
|
||||
store = SqliteStore(str(tmp_path / "ops.sqlite"))
|
||||
# Global ban
|
||||
store.set_setting("mcp_tool_wire_tool_policies", '{"mcp__echo__ping": 9999}')
|
||||
# Role override allows it (policy 0)
|
||||
store.set_setting("mcp_tool_wire_tool_policies_by_role", '{"ops":{"mcp__echo__ping":0}}')
|
||||
tools = [{"type": "function", "function": {"name": "mcp__echo__ping", "description": "Ping", "parameters": {"type": "object"}}}]
|
||||
out_ops = prepare_openai_tools_for_llm_api(tools, base_url="https://example", max_json_bytes=None, store=store, role="ops")
|
||||
out_gen = prepare_openai_tools_for_llm_api(tools, base_url="https://example", max_json_bytes=None, store=store, role="generalist")
|
||||
ops_names = {t["function"]["name"] for t in out_ops if isinstance(t, dict) and t.get("type") == "function"}
|
||||
gen_names = {t["function"]["name"] for t in out_gen if isinstance(t, dict) and t.get("type") == "function"}
|
||||
assert "mcp__echo__ping" in ops_names
|
||||
assert "mcp__echo__ping" not in gen_names
|
||||
|
||||
|
||||
def test_role_mode_unrestricted_disables_penalty_but_keeps_ban(tmp_path: Path) -> None:
|
||||
store = SqliteStore(str(tmp_path / "ops.sqlite"))
|
||||
store.set_setting("mcp_tool_wire_tool_policies", '{"mcp__echo__ping": 9999}')
|
||||
store.set_setting("mcp_tool_wire_role_mode_by_role", '{"manager":"unrestricted"}')
|
||||
tools = [{"type": "function", "function": {"name": "mcp__echo__ping", "description": "Ping", "parameters": {"type": "object"}}}]
|
||||
out_mgr = prepare_openai_tools_for_llm_api(tools, base_url="https://example", max_json_bytes=None, store=store, role="manager")
|
||||
names = {t["function"]["name"] for t in out_mgr if isinstance(t, dict) and t.get("type") == "function"}
|
||||
assert "mcp__echo__ping" not in names
|
||||
|
||||
|
||||
def test_role_mode_forbidden_disables_all_mcp(tmp_path: Path) -> None:
|
||||
store = SqliteStore(str(tmp_path / "ops.sqlite"))
|
||||
store.set_setting("mcp_tool_wire_role_mode_by_role", '{"ops":"forbidden"}')
|
||||
tools = [
|
||||
{"type": "function", "function": {"name": "mcp__echo__ping", "description": "Ping", "parameters": {"type": "object"}}},
|
||||
{"type": "function", "function": {"name": "system_time", "description": "t", "parameters": {"type": "object"}}},
|
||||
]
|
||||
out_ops = prepare_openai_tools_for_llm_api(tools, base_url="https://example", max_json_bytes=None, store=store, role="ops")
|
||||
names = {t["function"]["name"] for t in out_ops if isinstance(t, dict) and t.get("type") == "function"}
|
||||
assert "mcp__echo__ping" not in names
|
||||
assert "system_time" in names
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue