1111from . import __version__
1212from .audit import verify_audit_log
1313from .interfaces import CommandSource
14- from .mcp_tools import AUDIT_TOOL_NAME , MCP_READ_ONLY_TOOL_NAMES , POLICY_TOOL_NAME , McpToolName
14+ from .mcp_tools import (
15+ AUDIT_TOOL_NAME ,
16+ MCP_READ_ONLY_RESOURCE_URIS ,
17+ MCP_READ_ONLY_TOOL_NAMES ,
18+ POLICY_TOOL_NAME ,
19+ RUNBOOKS_SUMMARY_RESOURCE ,
20+ SKILLS_SUMMARY_RESOURCE ,
21+ McpResourceUri ,
22+ McpToolName ,
23+ )
1524from .policy import PolicyEngine
25+ from .runbooks import Runbook
1626from .security import redact_record
27+ from .skills import SkillManifest
1728
1829PROTOCOL_VERSION = "2025-06-18"
1930SERVER_NAME = "linuxagent-mcp"
4960 },
5061 },
5162}
63+ _RESOURCE_DEFINITIONS : dict [McpResourceUri , JsonObject ] = {
64+ RUNBOOKS_SUMMARY_RESOURCE : {
65+ "uri" : RUNBOOKS_SUMMARY_RESOURCE ,
66+ "name" : "LinuxAgent Runbook Summary" ,
67+ "description" : "Read-only summary of configured LinuxAgent runbooks." ,
68+ "mimeType" : "application/json" ,
69+ },
70+ SKILLS_SUMMARY_RESOURCE : {
71+ "uri" : SKILLS_SUMMARY_RESOURCE ,
72+ "name" : "LinuxAgent Skill Summary" ,
73+ "description" : "Read-only summary of configured LinuxAgent Skill manifests." ,
74+ "mimeType" : "application/json" ,
75+ },
76+ }
5277
5378JsonObject = dict [str , Any ]
5479
@@ -58,6 +83,9 @@ class McpServer:
5883 policy_engine : PolicyEngine
5984 audit_path : Path
6085 tools : tuple [McpToolName , ...] = MCP_READ_ONLY_TOOL_NAMES
86+ resources : tuple [McpResourceUri , ...] = MCP_READ_ONLY_RESOURCE_URIS
87+ runbooks : tuple [Runbook , ...] = ()
88+ skills : tuple [SkillManifest , ...] = ()
6189
6290 def handle (self , request : JsonObject ) -> JsonObject | None :
6391 method = request .get ("method" )
@@ -81,6 +109,10 @@ def _handle_request(self, method: str, request_id: Any, params: Any) -> JsonObje
81109 return _result (request_id , {"tools" : list (_tools (self .tools ))})
82110 if method == "tools/call" :
83111 return self ._call_tool (request_id , params )
112+ if method == "resources/list" :
113+ return _result (request_id , {"resources" : list (_resources (self .resources ))})
114+ if method == "resources/read" :
115+ return self ._read_resource (request_id , params )
84116 if method == "shutdown" :
85117 return _result (request_id , {})
86118 return _error (request_id , - 32601 , f"unknown method: { method } " )
@@ -100,6 +132,18 @@ def _call_tool(self, request_id: Any, params: Any) -> JsonObject:
100132 return _result (request_id , _tool_result (self ._verify_audit ()))
101133 return _error (request_id , - 32602 , f"unknown tool: { name } " )
102134
135+ def _read_resource (self , request_id : Any , params : Any ) -> JsonObject :
136+ if not isinstance (params , dict ):
137+ return _error (request_id , - 32602 , "resources/read params must be an object" )
138+ uri = params .get ("uri" )
139+ if uri not in self .resources :
140+ return _error (request_id , - 32602 , f"unknown or disabled resource: { uri } " )
141+ if uri == RUNBOOKS_SUMMARY_RESOURCE :
142+ return _result (request_id , _resource_result (uri , _runbook_summary (self .runbooks )))
143+ if uri == SKILLS_SUMMARY_RESOURCE :
144+ return _result (request_id , _resource_result (uri , _skill_summary (self .skills )))
145+ return _error (request_id , - 32602 , f"unknown resource: { uri } " )
146+
103147 def _classify (self , arguments : JsonObject ) -> JsonObject :
104148 command = arguments .get ("command" )
105149 if not isinstance (command , str ) or not command :
@@ -173,7 +217,10 @@ def _initialize_result(params: Any) -> JsonObject:
173217 protocol_version = str (params ["protocolVersion" ])
174218 return {
175219 "protocolVersion" : protocol_version ,
176- "capabilities" : {"tools" : {"listChanged" : False }},
220+ "capabilities" : {
221+ "tools" : {"listChanged" : False },
222+ "resources" : {"subscribe" : False , "listChanged" : False },
223+ },
177224 "serverInfo" : {"name" : SERVER_NAME , "version" : __version__ },
178225 }
179226
@@ -182,6 +229,59 @@ def _tools(names: tuple[McpToolName, ...]) -> tuple[JsonObject, ...]:
182229 return tuple (_TOOL_DEFINITIONS [name ] for name in names )
183230
184231
232+ def _resources (uris : tuple [McpResourceUri , ...]) -> tuple [JsonObject , ...]:
233+ return tuple (_RESOURCE_DEFINITIONS [uri ] for uri in uris )
234+
235+
236+ def _resource_result (uri : str , payload : JsonObject ) -> JsonObject :
237+ text = json .dumps (redact_record (payload ), ensure_ascii = False , sort_keys = True )
238+ return {
239+ "contents" : [
240+ {
241+ "uri" : uri ,
242+ "mimeType" : "application/json" ,
243+ "text" : text ,
244+ }
245+ ]
246+ }
247+
248+
249+ def _runbook_summary (runbooks : tuple [Runbook , ...]) -> JsonObject :
250+ return {
251+ "runbooks" : [
252+ {
253+ "id" : runbook .id ,
254+ "title" : runbook .title ,
255+ "steps" : [
256+ {
257+ "purpose" : step .purpose ,
258+ "read_only" : step .read_only ,
259+ }
260+ for step in runbook .steps
261+ ],
262+ }
263+ for runbook in runbooks
264+ ]
265+ }
266+
267+
268+ def _skill_summary (skills : tuple [SkillManifest , ...]) -> JsonObject :
269+ return {
270+ "enabled" : bool (skills ),
271+ "skills" : [
272+ {
273+ "name" : skill .name ,
274+ "version" : skill .version ,
275+ "description" : skill .description ,
276+ "permissions" : list (skill .permissions ),
277+ "has_planner_guidance" : bool (skill .planner_guidance ),
278+ "runbooks" : [runbook .id for runbook in skill .runbooks ],
279+ }
280+ for skill in skills
281+ ],
282+ }
283+
284+
185285def _tool_result (payload : JsonObject ) -> JsonObject :
186286 return {
187287 "content" : payload ["content" ],
0 commit comments