File indexing completed on 2026-09-28 09:37:31
0001 """The MCP tool set served on the external face (docs/live-data-access.md).
0002
0003 The relay forwards only the tools named here: tools/list results are
0004 filtered to the set and a tools/call for any other name is refused before
0005 it crosses the tunnel. The set is the read-only tools plus one write,
0006 ai_propose_ping, which creates a proposal that takes effect only when a
0007 person accepts it on the alarm dashboard; it is the way a named collaborator
0008 raises an obligation from outside. Not served: the testbed tools (swf_*),
0009 which control and inspect internal processes; the PCS intake and lifecycle
0010 mutations, driven by the production bot; ai_decide_proposal, whose approve
0011 executes the proposal; and epic_register_ai_assessment, written by the
0012 assessment services. A new monitor tool reaches the outside only when it is
0013 added here.
0014 """
0015
0016 import json
0017
0018 from django.http import HttpResponse
0019
0020 _RUCIO = (
0021 'list_scopes', 'list_dids', 'list_files', 'list_content', 'get_did_metadata',
0022 'summarize_datasets', 'get_account_limits', 'get_account_usage', 'list_rses',
0023 'get_rse_usage', 'list_rules', 'get_rule_locks', 'list_file_replicas',
0024 'extract_scope',
0025 )
0026
0027 EXTERNAL_TOOLS = frozenset(
0028 [
0029 'get_server_instructions',
0030
0031 'panda_get_activity', 'panda_list_jobs', 'panda_diagnose_jobs',
0032 'panda_list_tasks', 'panda_error_summary', 'panda_study_job',
0033 'panda_list_queues', 'panda_get_queue', 'panda_resource_usage',
0034 'panda_harvester_workers',
0035
0036 'pcs_list_tags', 'pcs_get_tag', 'pcs_search_tags',
0037 'pcs_dataset_list', 'pcs_dataset_get', 'pcs_data_provenance',
0038 'pcs_prodtask_list', 'pcs_prodtask_get', 'pcs_prodtask_artifact',
0039
0040 'epicprod_campaign_status', 'epicprod_list_actions',
0041
0042 'snapper_latest', 'snapper_state_at', 'snapper_component_history',
0043 'snapper_changes_between', 'snapper_context_around', 'snapper_series',
0044 'snapper_cut_summary',
0045
0046 'epic_get_ai_content', 'ai_list_proposals', 'ai_propose_ping',
0047 ]
0048 + [f'jlab_rucio_{s}' for s in _RUCIO]
0049 + [f'bnl_rucio_{s}' for s in _RUCIO]
0050 )
0051
0052
0053 def _rpc_error(req_id, message):
0054 body = {'jsonrpc': '2.0', 'id': req_id,
0055 'error': {'code': -32602, 'message': message}}
0056 return HttpResponse(json.dumps(body), content_type='application/json')
0057
0058
0059 def refuse_call(body_bytes):
0060 """A JSON-RPC error response if the request calls a tool outside the
0061 external set, else None. Unparseable bodies pass through for the
0062 upstream to answer."""
0063 try:
0064 req = json.loads(body_bytes or b'')
0065 except (ValueError, UnicodeDecodeError):
0066 return None
0067 reqs = req if isinstance(req, list) else [req]
0068 for r in reqs:
0069 if not isinstance(r, dict) or r.get('method') != 'tools/call':
0070 continue
0071 name = (r.get('params') or {}).get('name')
0072 if name not in EXTERNAL_TOOLS:
0073 return _rpc_error(r.get('id'),
0074 f'tool {name!r} is not served on the external face')
0075 return None
0076
0077
0078 def filter_tools_list(body_bytes, content_type):
0079 """Drop tools outside the external set from a tools/list result."""
0080 if 'json' not in (content_type or ''):
0081 return body_bytes
0082 try:
0083 resp = json.loads(body_bytes)
0084 except (ValueError, UnicodeDecodeError):
0085 return body_bytes
0086 resps = resp if isinstance(resp, list) else [resp]
0087 changed = False
0088 for r in resps:
0089 tools = (r.get('result') or {}).get('tools') if isinstance(r, dict) else None
0090 if isinstance(tools, list):
0091 r['result']['tools'] = [t for t in tools if t.get('name') in EXTERNAL_TOOLS]
0092 changed = True
0093 return json.dumps(resp).encode() if changed else body_bytes