mirror of
https://github.com/hansjone/netx.git
synced 2026-10-18 15:00:45 +08:00
feat(mcp): publish ops skills via /mcp resources, prompts and getNetxSkill
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
42d5f9c5a7
commit
166d2289c3
11 changed files with 1057 additions and 25 deletions
|
|
@ -7,16 +7,37 @@ from importlib.metadata import version as pkg_version
|
|||
from typing import Any
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
from mcp.shared.exceptions import MCPError
|
||||
from mcp.types import (
|
||||
CallToolRequestParams,
|
||||
CallToolResult,
|
||||
GetPromptRequestParams,
|
||||
GetPromptResult,
|
||||
INVALID_PARAMS,
|
||||
ListPromptsResult,
|
||||
ListResourcesResult,
|
||||
ListToolsResult,
|
||||
PaginatedRequestParams,
|
||||
Prompt,
|
||||
PromptMessage,
|
||||
ReadResourceRequestParams,
|
||||
ReadResourceResult,
|
||||
Resource,
|
||||
TextContent,
|
||||
TextResourceContents,
|
||||
)
|
||||
|
||||
from .context import get_request_context
|
||||
from .registry import TOOL_REQUIRED_SCOPE, call_tool, to_mcp_tool, tools_for_request
|
||||
from .skills import (
|
||||
build_instructions,
|
||||
list_visible_skill_files,
|
||||
list_visible_skills,
|
||||
prompt_skill_md_text,
|
||||
read_skill_text,
|
||||
resolve_uri,
|
||||
resource_dict,
|
||||
)
|
||||
|
||||
|
||||
def netx_version() -> str:
|
||||
|
|
@ -103,19 +124,115 @@ async def _on_call_tool(
|
|||
return CallToolResult(content=texts, is_error=is_error)
|
||||
|
||||
|
||||
async def _on_list_resources(
|
||||
ctx: ServerRequestContext,
|
||||
params: PaginatedRequestParams | None,
|
||||
) -> ListResourcesResult:
|
||||
scopes, groups = _scopes_and_groups()
|
||||
resources: list[Resource] = []
|
||||
for sf in list_visible_skill_files(scopes, groups):
|
||||
d = resource_dict(sf)
|
||||
resources.append(
|
||||
Resource(
|
||||
uri=d["uri"],
|
||||
name=d["name"],
|
||||
mimeType=d.get("mimeType"),
|
||||
description=d.get("description"),
|
||||
meta=d.get("_meta"),
|
||||
)
|
||||
)
|
||||
return ListResourcesResult(resources=resources)
|
||||
|
||||
|
||||
async def _on_read_resource(
|
||||
ctx: ServerRequestContext,
|
||||
params: ReadResourceRequestParams,
|
||||
) -> ReadResourceResult:
|
||||
scopes, groups = _scopes_and_groups()
|
||||
uri = str(params.uri or "")
|
||||
sf = resolve_uri(uri, scopes, groups)
|
||||
if sf is None:
|
||||
raise MCPError(INVALID_PARAMS, "resource not found or not authorized")
|
||||
try:
|
||||
text = read_skill_text(sf)
|
||||
except OSError:
|
||||
raise MCPError(INVALID_PARAMS, "resource not found or not authorized") from None
|
||||
return ReadResourceResult(
|
||||
contents=[
|
||||
TextResourceContents(
|
||||
uri=sf.uri,
|
||||
mimeType="text/markdown",
|
||||
text=text,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def _on_list_prompts(
|
||||
ctx: ServerRequestContext,
|
||||
params: PaginatedRequestParams | None,
|
||||
) -> ListPromptsResult:
|
||||
scopes, groups = _scopes_and_groups()
|
||||
prompts = [
|
||||
Prompt(name=name, description=desc or None)
|
||||
for name, _group, desc in list_visible_skills(scopes, groups)
|
||||
]
|
||||
return ListPromptsResult(prompts=prompts)
|
||||
|
||||
|
||||
async def _on_get_prompt(
|
||||
ctx: ServerRequestContext,
|
||||
params: GetPromptRequestParams,
|
||||
) -> GetPromptResult:
|
||||
scopes, groups = _scopes_and_groups()
|
||||
name = str(params.name or "").strip()
|
||||
text = prompt_skill_md_text(name, scopes, groups)
|
||||
if text is None:
|
||||
raise MCPError(INVALID_PARAMS, "prompt not found or not authorized")
|
||||
skills = {n: d for n, _g, d in list_visible_skills(scopes, groups)}
|
||||
return GetPromptResult(
|
||||
description=skills.get(name) or None,
|
||||
messages=[
|
||||
PromptMessage(
|
||||
role="user",
|
||||
content=TextContent(type="text", text=text),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def build_mcp_server() -> Server[Any]:
|
||||
return Server(
|
||||
server: Server[Any] = Server(
|
||||
"netx",
|
||||
version=netx_version(),
|
||||
instructions=(
|
||||
"netx remote MCP: ops / topology / bizMonitor tools. "
|
||||
"Filter with ?groups=ops,topology,bizMonitor. "
|
||||
"Authorize with Bearer MCP token or user JWT."
|
||||
),
|
||||
instructions=_BASE_INSTRUCTIONS_STATIC,
|
||||
on_list_tools=_on_list_tools,
|
||||
on_call_tool=_on_call_tool,
|
||||
on_list_resources=_on_list_resources,
|
||||
on_read_resource=_on_read_resource,
|
||||
on_list_prompts=_on_list_prompts,
|
||||
on_get_prompt=_on_get_prompt,
|
||||
)
|
||||
|
||||
# Server.instructions is fixed at construct time; override init options so
|
||||
# each initialize sees a catalog filtered by the current token / ?groups=.
|
||||
_orig_create = server.create_initialization_options
|
||||
|
||||
def _create_initialization_options(*args: Any, **kwargs: Any) -> Any:
|
||||
opts = _orig_create(*args, **kwargs)
|
||||
scopes, groups = _scopes_and_groups()
|
||||
return opts.model_copy(update={"instructions": build_instructions(scopes, groups)})
|
||||
|
||||
server.create_initialization_options = _create_initialization_options # type: ignore[method-assign]
|
||||
return server
|
||||
|
||||
|
||||
_BASE_INSTRUCTIONS_STATIC = (
|
||||
"netx remote MCP: ops / topology / bizMonitor tools. "
|
||||
"Filter with ?groups=ops,topology,bizMonitor. "
|
||||
"Authorize with Bearer MCP token or user JWT."
|
||||
)
|
||||
|
||||
|
||||
# Singleton used by mount; streamable_http_app() must be called once so session_manager exists.
|
||||
mcp_server = build_mcp_server()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue