Import PoC4 MCP test UI source snapshot

This commit is contained in:
devmrko
2026-07-14 14:09:31 +09:00
parent b48d9a0792
commit 5bdd24299e
17 changed files with 8971 additions and 3 deletions

View File

@@ -0,0 +1,190 @@
"""LLM-based MCP tool routing.
The router receives only the user question and the discovered MCP tool
descriptors. It never receives MCP bearer tokens or provider credentials.
"""
from __future__ import annotations
from dataclasses import dataclass
import json
from typing import Any, Mapping
from src.oci_genai_sdk import (
build_oci_genai_completion_client,
temperature_for_model_profile,
)
from src.poc3.model_registry import resolve_model_profile
@dataclass(frozen=True)
class McpTool:
name: str
description: str
schema: Mapping[str, Any]
read_only: bool
@dataclass(frozen=True)
class RoutedMcpTool:
server_id: str
tool: McpTool
class McpToolRouterError(RuntimeError):
"""Safe routing error. Must not contain secrets or provider traces."""
def route_mcp_tool_across_servers_with_llm(
tools: list[RoutedMcpTool],
question: str,
*,
router_model_profile: str,
) -> RoutedMcpTool:
"""Select one discovered MCP server/tool pair with OCI GenAI."""
candidates = list(tools)
if not candidates:
raise McpToolRouterError("라우팅 가능한 MCP tool이 없습니다.")
by_key = {
"{}::{}".format(candidate.server_id, candidate.tool.name): candidate
for candidate in candidates
}
route_keys = list(by_key)
try:
profile = resolve_model_profile(router_model_profile)
client = build_oci_genai_completion_client(
profile.model_id,
profile.answer_model_region,
profile.answer_model_endpoint,
)
tool_catalog = [
{
"route_key": "{}::{}".format(candidate.server_id, candidate.tool.name),
"server_id": candidate.server_id,
"tool_name": candidate.tool.name,
"description": candidate.tool.description[:1000],
"input_properties": sorted(
(
candidate.tool.schema.get("properties", {})
if isinstance(
candidate.tool.schema.get("properties"), Mapping
)
else {}
).keys()
),
"read_only": candidate.tool.read_only,
}
for candidate in candidates
]
text = client.complete(
system_prompt=(
"You are an MCP server and tool router. Choose exactly one "
"server/tool route for the user question from the discovered "
"routes. Return only JSON that matches the schema. Never "
"request or expose bearer tokens. Do not invent server ids or "
"tool names."
),
user_prompt=json.dumps(
{
"question": question,
"routes": tool_catalog,
},
ensure_ascii=False,
),
response_schema={
"type": "object",
"additionalProperties": False,
"required": ["route_key"],
"properties": {
"route_key": {
"type": "string",
"enum": route_keys,
}
},
},
max_tokens=256,
temperature=temperature_for_model_profile(profile),
)
routed = json.loads(text)
except Exception:
raise McpToolRouterError("LLM tool router 호출에 실패했습니다.") from None
if not isinstance(routed, Mapping):
raise McpToolRouterError("LLM tool router 응답 형식이 올바르지 않습니다.")
selected_key = str(routed.get("route_key") or "").strip()
selected = by_key.get(selected_key)
if selected is None:
raise McpToolRouterError("LLM tool router가 허용되지 않은 route를 선택했습니다.")
return selected
def route_mcp_tool_with_llm(
tools: list[McpTool],
question: str,
*,
preferred_tool: str,
tool_allowlist: tuple[str, ...],
router_model_profile: str,
) -> McpTool:
"""Backward-compatible single-server routing helper."""
candidates = [
tool for tool in tools if not tool_allowlist or tool.name in tool_allowlist
]
routed = route_mcp_tool_across_servers_with_llm(
[RoutedMcpTool(server_id="default", tool=tool) for tool in candidates],
question,
router_model_profile=router_model_profile,
)
return routed.tool
def build_mcp_tool_arguments(
tool: McpTool,
question: str,
limit: int,
*,
preferred_tool: str,
) -> dict[str, Any]:
"""Build bounded tool arguments from the selected tool schema."""
properties = tool.schema.get("properties")
if not isinstance(properties, Mapping):
properties = {}
if tool.name == preferred_tool:
return {"prompt": question, "limit": limit}
if "prompt" in properties:
args: dict[str, Any] = {"prompt": question}
if "limit" in properties:
args["limit"] = limit
elif "max_rows" in properties:
args["max_rows"] = limit
return args
if "question" in properties:
args = {"question": question}
if "max_rows" in properties:
args["max_rows"] = limit
elif "limit" in properties:
args["limit"] = limit
return args
if "query" in properties:
args = {"query": question}
if "max_evidence" in properties:
args["max_evidence"] = min(limit, 10)
elif "limit" in properties:
args["limit"] = limit
return args
return {"prompt": question, "limit": limit}
__all__ = [
"McpTool",
"McpToolRouterError",
"RoutedMcpTool",
"build_mcp_tool_arguments",
"route_mcp_tool_across_servers_with_llm",
"route_mcp_tool_with_llm",
]