614 lines
22 KiB
Go
614 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"inou/lib"
|
|
)
|
|
|
|
// MCP HTTP Server Implementation
|
|
// Implements Streamable HTTP transport for Anthropic's Connectors Directory
|
|
// See: https://modelcontextprotocol.io/specification/draft/basic/authorization
|
|
|
|
const (
|
|
mcpProtocolVersion = "2025-06-18"
|
|
mcpServerName = "inou-health"
|
|
mcpServerVersion = "1.0.0"
|
|
)
|
|
|
|
// OAuth 2.0 Protected Resource Metadata (RFC9728)
|
|
// GET /.well-known/oauth-protected-resource
|
|
func handleOAuthProtectedResource(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Determine the base URL from the request
|
|
scheme := "https"
|
|
if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
|
|
scheme = "http"
|
|
}
|
|
host := r.Host
|
|
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
|
|
|
metadata := map[string]interface{}{
|
|
"resource": baseURL + "/mcp",
|
|
"authorization_servers": []string{baseURL},
|
|
"scopes_supported": []string{"read"},
|
|
"bearer_methods_supported": []string{"header"},
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "max-age=3600")
|
|
json.NewEncoder(w).Encode(metadata)
|
|
}
|
|
|
|
// OAuth 2.0 Authorization Server Metadata (RFC8414)
|
|
// GET /.well-known/oauth-authorization-server
|
|
func handleOAuthAuthorizationServer(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
scheme := "https"
|
|
if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
|
|
scheme = "http"
|
|
}
|
|
host := r.Host
|
|
baseURL := fmt.Sprintf("%s://%s", scheme, host)
|
|
|
|
metadata := map[string]interface{}{
|
|
"issuer": baseURL,
|
|
"authorization_endpoint": baseURL + "/oauth/authorize",
|
|
"token_endpoint": baseURL + "/oauth/token",
|
|
"userinfo_endpoint": baseURL + "/oauth/userinfo",
|
|
"revocation_endpoint": baseURL + "/oauth/revoke",
|
|
"registration_endpoint": baseURL + "/register",
|
|
"response_types_supported": []string{"code"},
|
|
"grant_types_supported": []string{"authorization_code", "refresh_token"},
|
|
"token_endpoint_auth_methods_supported": []string{"none", "client_secret_post"},
|
|
"code_challenge_methods_supported": []string{"S256"},
|
|
"scopes_supported": []string{"read"},
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("Cache-Control", "max-age=3600")
|
|
json.NewEncoder(w).Encode(metadata)
|
|
}
|
|
|
|
// Dynamic Client Registration (RFC 7591)
|
|
// POST /register - Allows clients to register themselves dynamically
|
|
func handleDynamicClientRegistration(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Parse registration request
|
|
var req struct {
|
|
RedirectURIs []string `json:"redirect_uris"`
|
|
ClientName string `json:"client_name"`
|
|
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
|
GrantTypes []string `json:"grant_types"`
|
|
ResponseTypes []string `json:"response_types"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"error": "invalid_client_metadata",
|
|
"error_description": "Invalid JSON body",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validate redirect URIs
|
|
if len(req.RedirectURIs) == 0 {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"error": "invalid_redirect_uri",
|
|
"error_description": "At least one redirect_uri is required",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Create client name if not provided
|
|
clientName := req.ClientName
|
|
if clientName == "" {
|
|
clientName = "Dynamic Client"
|
|
}
|
|
|
|
// Create the client
|
|
client, secret, err := lib.OAuthClientCreate(clientName, req.RedirectURIs)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
"error": "server_error",
|
|
"error_description": "Failed to create client",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Return registration response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusCreated)
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"client_id": client.ClientID,
|
|
"client_secret": secret,
|
|
"client_name": clientName,
|
|
"redirect_uris": req.RedirectURIs,
|
|
"token_endpoint_auth_method": "client_secret_post",
|
|
"grant_types": []string{"authorization_code", "refresh_token"},
|
|
"response_types": []string{"code"},
|
|
})
|
|
}
|
|
|
|
// MCP JSON-RPC types
|
|
type mcpRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID interface{} `json:"id"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params,omitempty"`
|
|
}
|
|
|
|
type mcpResponse struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
ID interface{} `json:"id"`
|
|
Result interface{} `json:"result,omitempty"`
|
|
Error *mcpError `json:"error,omitempty"`
|
|
}
|
|
|
|
type mcpError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// MCP HTTP endpoint
|
|
// POST /mcp - Streamable HTTP transport
|
|
func handleMCP(w http.ResponseWriter, r *http.Request) {
|
|
// Handle CORS preflight
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
if r.Method != "POST" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Check authorization
|
|
auth := r.Header.Get("Authorization")
|
|
scheme := "https"
|
|
if r.TLS == nil && r.Header.Get("X-Forwarded-Proto") != "https" {
|
|
scheme = "http"
|
|
}
|
|
baseURL := fmt.Sprintf("%s://%s", scheme, r.Host)
|
|
|
|
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
|
|
w.Header().Set("WWW-Authenticate", fmt.Sprintf(
|
|
`Bearer resource_metadata="%s/.well-known/oauth-protected-resource", scope="read"`,
|
|
baseURL,
|
|
))
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
tokenStr := strings.TrimPrefix(auth, "Bearer ")
|
|
token, err := lib.TokenParse(tokenStr)
|
|
if err != nil {
|
|
w.Header().Set("WWW-Authenticate", fmt.Sprintf(
|
|
`Bearer resource_metadata="%s/.well-known/oauth-protected-resource", error="invalid_token"`,
|
|
baseURL,
|
|
))
|
|
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("[MCP] OAuth token authenticated as dossier: %s\n", token.DossierID)
|
|
|
|
// Store the raw access token for passing to internal API calls
|
|
accessToken := tokenStr
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
sendMCPError(w, nil, -32700, "Parse error")
|
|
return
|
|
}
|
|
|
|
var req mcpRequest
|
|
if err := json.Unmarshal(body, &req); err != nil {
|
|
sendMCPError(w, nil, -32700, "Parse error")
|
|
return
|
|
}
|
|
|
|
// Handle MCP methods
|
|
switch req.Method {
|
|
case "initialize":
|
|
handleMCPInitialize(w, req)
|
|
case "notifications/initialized":
|
|
// No response needed for notifications
|
|
w.WriteHeader(http.StatusNoContent)
|
|
case "tools/list":
|
|
handleMCPToolsList(w, req)
|
|
case "tools/call":
|
|
handleMCPToolsCall(w, req, accessToken, token.DossierID)
|
|
default:
|
|
sendMCPError(w, req.ID, -32601, "Method not found: "+req.Method)
|
|
}
|
|
}
|
|
|
|
func sendMCPError(w http.ResponseWriter, id interface{}, code int, message string) {
|
|
resp := mcpResponse{
|
|
JSONRPC: "2.0",
|
|
ID: id,
|
|
Error: &mcpError{Code: code, Message: message},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func sendMCPResult(w http.ResponseWriter, id interface{}, result interface{}) {
|
|
resp := mcpResponse{
|
|
JSONRPC: "2.0",
|
|
ID: id,
|
|
Result: result,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func handleMCPInitialize(w http.ResponseWriter, req mcpRequest) {
|
|
sendMCPResult(w, req.ID, map[string]interface{}{
|
|
"protocolVersion": mcpProtocolVersion,
|
|
"capabilities": map[string]interface{}{
|
|
"tools": map[string]interface{}{},
|
|
},
|
|
"serverInfo": map[string]interface{}{
|
|
"name": mcpServerName,
|
|
"version": mcpServerVersion,
|
|
},
|
|
})
|
|
}
|
|
|
|
func handleMCPToolsList(w http.ResponseWriter, req mcpRequest) {
|
|
readOnly := map[string]interface{}{"readOnlyHint": true}
|
|
|
|
tools := []map[string]interface{}{
|
|
{
|
|
"name": "list_dossiers",
|
|
"description": "List all patient dossiers accessible to this account.",
|
|
"inputSchema": map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "list_studies",
|
|
"description": "List all imaging studies for a patient dossier.",
|
|
"inputSchema": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
|
},
|
|
"required": []string{"dossier"},
|
|
},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "list_series",
|
|
"description": "List series for a study. Filter by description (AX, T1, FLAIR, etc).",
|
|
"inputSchema": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
|
"study": map[string]interface{}{"type": "string", "description": "Study ID (16-char hex)"},
|
|
"filter": map[string]interface{}{"type": "string", "description": "Filter by description"},
|
|
},
|
|
"required": []string{"dossier", "study"},
|
|
},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "list_slices",
|
|
"description": "List slices for a series with position info.",
|
|
"inputSchema": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
|
"series": map[string]interface{}{"type": "string", "description": "Series ID (16-char hex)"},
|
|
},
|
|
"required": []string{"dossier", "series"},
|
|
},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "fetch_image",
|
|
"description": "Fetch slice image as base64 PNG. Optionally set window/level.",
|
|
"inputSchema": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
|
"slice": map[string]interface{}{"type": "string", "description": "Slice ID (16-char hex)"},
|
|
"wc": map[string]interface{}{"type": "number", "description": "Window center"},
|
|
"ww": map[string]interface{}{"type": "number", "description": "Window width"},
|
|
},
|
|
"required": []string{"dossier", "slice"},
|
|
},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "fetch_contact_sheet",
|
|
"description": "Fetch contact sheet (thumbnail grid) for NAVIGATION ONLY. Use to identify slices, then fetch at full resolution. NEVER diagnose from thumbnails.",
|
|
"inputSchema": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
|
"series": map[string]interface{}{"type": "string", "description": "Series ID (16-char hex)"},
|
|
"wc": map[string]interface{}{"type": "number", "description": "Window center"},
|
|
"ww": map[string]interface{}{"type": "number", "description": "Window width"},
|
|
},
|
|
"required": []string{"dossier", "series"},
|
|
},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "get_categories",
|
|
"description": "List available data categories for a dossier (imaging, labs, documents, genome). Use this first to discover what data types are available, then query entries by category. For lab results, LOINC codes provide best search accuracy.",
|
|
"inputSchema": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
|
"type": map[string]interface{}{"type": "string", "description": "Observation type (e.g., genome) for subcategories"},
|
|
"category": map[string]interface{}{"type": "string", "description": "Get subcategories within this category"},
|
|
},
|
|
"required": []string{"dossier"},
|
|
},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "query_entries",
|
|
"description": "Query entries for any category (labs, documents, etc.). For imaging, use list_studies/list_series/list_slices. For genome, use query_genome. For labs: Use search_key with LOINC code (e.g., '2947-0') for fast, accurate results. Use get_categories first to discover available categories.",
|
|
"inputSchema": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
|
"category": map[string]interface{}{"type": "string", "description": "Category: 'labs', 'documents', etc. (use get_categories to list)"},
|
|
"type": map[string]interface{}{"type": "string", "description": "Type within category (e.g., test name for labs)"},
|
|
"search_key": map[string]interface{}{"type": "string", "description": "LOINC code for labs (e.g., '2947-0'), gene name for genome (e.g., 'MTHFR')"},
|
|
"parent": map[string]interface{}{"type": "string", "description": "Parent entry ID for hierarchical queries"},
|
|
"from": map[string]interface{}{"type": "string", "description": "Timestamp start (Unix seconds)"},
|
|
"to": map[string]interface{}{"type": "string", "description": "Timestamp end (Unix seconds)"},
|
|
"limit": map[string]interface{}{"type": "number", "description": "Maximum results"},
|
|
},
|
|
"required": []string{"dossier"},
|
|
},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "query_genome",
|
|
"description": "Query genome variants with specialized genome parameters (magnitude, repute, gene, rsids). Returns matches with significance ratings. For non-genome data, use get_categories to find available categories.",
|
|
"inputSchema": map[string]interface{}{
|
|
"type": "object",
|
|
"properties": map[string]interface{}{
|
|
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
|
"gene": map[string]interface{}{"type": "string", "description": "Gene name(s), comma-separated"},
|
|
"search": map[string]interface{}{"type": "string", "description": "Search gene, subcategory, or summary"},
|
|
"category": map[string]interface{}{"type": "string", "description": "Filter by category"},
|
|
"rsids": map[string]interface{}{"type": "string", "description": "Comma-separated rsids"},
|
|
"min_magnitude": map[string]interface{}{"type": "number", "description": "Minimum magnitude"},
|
|
"include_hidden": map[string]interface{}{"type": "boolean", "description": "Include hidden categories and high-magnitude variants"},
|
|
},
|
|
"required": []string{"dossier"},
|
|
},
|
|
"annotations": readOnly,
|
|
},
|
|
{
|
|
"name": "get_version",
|
|
"description": "Get bridge and server version info.",
|
|
"inputSchema": map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
|
"annotations": readOnly,
|
|
},
|
|
}
|
|
|
|
sendMCPResult(w, req.ID, map[string]interface{}{"tools": tools})
|
|
}
|
|
|
|
func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, dossierID string) {
|
|
var params struct {
|
|
Name string `json:"name"`
|
|
Arguments map[string]interface{} `json:"arguments"`
|
|
}
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
sendMCPError(w, req.ID, -32602, "Invalid params")
|
|
return
|
|
}
|
|
|
|
// Use accessToken for internal API calls (dossierID available if needed)
|
|
_ = dossierID
|
|
|
|
switch params.Name {
|
|
case "list_dossiers":
|
|
result, err := mcpListDossiers(accessToken)
|
|
if err != nil {
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
sendMCPResult(w, req.ID, mcpTextContent(result))
|
|
|
|
case "list_studies":
|
|
dossier, _ := params.Arguments["dossier"].(string)
|
|
if dossier == "" {
|
|
sendMCPError(w, req.ID, -32602, "dossier required")
|
|
return
|
|
}
|
|
result, err := mcpListStudies(accessToken, dossier)
|
|
if err != nil {
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
sendMCPResult(w, req.ID, mcpTextContent(result))
|
|
|
|
case "list_series":
|
|
dossier, _ := params.Arguments["dossier"].(string)
|
|
study, _ := params.Arguments["study"].(string)
|
|
if dossier == "" || study == "" {
|
|
sendMCPError(w, req.ID, -32602, "dossier and study required")
|
|
return
|
|
}
|
|
result, err := mcpListSeries(accessToken, dossier, study)
|
|
if err != nil {
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
sendMCPResult(w, req.ID, mcpTextContent(result))
|
|
|
|
case "list_slices":
|
|
dossier, _ := params.Arguments["dossier"].(string)
|
|
series, _ := params.Arguments["series"].(string)
|
|
if dossier == "" || series == "" {
|
|
sendMCPError(w, req.ID, -32602, "dossier and series required")
|
|
return
|
|
}
|
|
result, err := mcpListSlices(accessToken, dossier, series)
|
|
if err != nil {
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
sendMCPResult(w, req.ID, mcpTextContent(result))
|
|
|
|
case "fetch_image":
|
|
dossier, _ := params.Arguments["dossier"].(string)
|
|
slice, _ := params.Arguments["slice"].(string)
|
|
if dossier == "" || slice == "" {
|
|
sendMCPError(w, req.ID, -32602, "dossier and slice required")
|
|
return
|
|
}
|
|
wc, _ := params.Arguments["wc"].(float64)
|
|
ww, _ := params.Arguments["ww"].(float64)
|
|
result, err := mcpFetchImage(accessToken, dossier, slice, wc, ww)
|
|
if err != nil {
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
sendMCPResult(w, req.ID, result)
|
|
|
|
case "fetch_contact_sheet":
|
|
dossier, _ := params.Arguments["dossier"].(string)
|
|
series, _ := params.Arguments["series"].(string)
|
|
if dossier == "" || series == "" {
|
|
sendMCPError(w, req.ID, -32602, "dossier and series required")
|
|
return
|
|
}
|
|
wc, _ := params.Arguments["wc"].(float64)
|
|
ww, _ := params.Arguments["ww"].(float64)
|
|
result, err := mcpFetchContactSheet(accessToken, dossier, series, wc, ww)
|
|
if err != nil {
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
sendMCPResult(w, req.ID, result)
|
|
|
|
case "query_entries":
|
|
dossier, _ := params.Arguments["dossier"].(string)
|
|
if dossier == "" {
|
|
sendMCPError(w, req.ID, -32602, "dossier required")
|
|
return
|
|
}
|
|
category, _ := params.Arguments["category"].(string)
|
|
typ, _ := params.Arguments["type"].(string)
|
|
searchKey, _ := params.Arguments["search_key"].(string)
|
|
parent, _ := params.Arguments["parent"].(string)
|
|
from, _ := params.Arguments["from"].(string)
|
|
to, _ := params.Arguments["to"].(string)
|
|
limit, _ := params.Arguments["limit"].(float64)
|
|
result, err := mcpQueryEntries(accessToken, dossier, category, typ, searchKey, parent, from, to, int(limit))
|
|
if err != nil {
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
sendMCPResult(w, req.ID, mcpTextContent(result))
|
|
|
|
case "get_categories":
|
|
dossier, _ := params.Arguments["dossier"].(string)
|
|
if dossier == "" {
|
|
sendMCPError(w, req.ID, -32602, "dossier required")
|
|
return
|
|
}
|
|
typ, _ := params.Arguments["type"].(string)
|
|
category, _ := params.Arguments["category"].(string)
|
|
result, err := mcpGetCategories(accessToken, dossier, typ, category)
|
|
if err != nil {
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
sendMCPResult(w, req.ID, mcpTextContent(result))
|
|
|
|
case "query_genome":
|
|
dossier, _ := params.Arguments["dossier"].(string)
|
|
if dossier == "" {
|
|
sendMCPError(w, req.ID, -32602, "dossier required")
|
|
return
|
|
}
|
|
gene, _ := params.Arguments["gene"].(string)
|
|
search, _ := params.Arguments["search"].(string)
|
|
category, _ := params.Arguments["category"].(string)
|
|
rsids, _ := params.Arguments["rsids"].(string)
|
|
minMag, _ := params.Arguments["min_magnitude"].(float64)
|
|
includeHidden, _ := params.Arguments["include_hidden"].(bool)
|
|
|
|
fmt.Printf("[MCP] query_genome: dossier=%s gene=%s category=%s includeHidden=%v\n",
|
|
dossier, gene, category, includeHidden)
|
|
|
|
result, err := mcpQueryGenome(accessToken, dossier, gene, search, category, rsids, minMag, includeHidden)
|
|
if err != nil {
|
|
fmt.Printf("[MCP] query_genome error: %v\n", err)
|
|
sendMCPError(w, req.ID, -32000, err.Error())
|
|
return
|
|
}
|
|
fmt.Printf("[MCP] query_genome success: %d bytes\n", len(result))
|
|
sendMCPResult(w, req.ID, mcpTextContent(result))
|
|
|
|
case "get_version":
|
|
sendMCPResult(w, req.ID, mcpTextContent(fmt.Sprintf("Server: %s v%s", mcpServerName, mcpServerVersion)))
|
|
|
|
default:
|
|
sendMCPError(w, req.ID, -32601, "Unknown tool: "+params.Name)
|
|
}
|
|
}
|
|
|
|
func mcpTextContent(text string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"content": []map[string]interface{}{
|
|
{"type": "text", "text": text},
|
|
},
|
|
}
|
|
}
|
|
|
|
func mcpImageContent(base64Data, mimeType, text string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"content": []map[string]interface{}{
|
|
{"type": "image", "data": base64Data, "mimeType": mimeType},
|
|
{"type": "text", "text": text},
|
|
},
|
|
}
|
|
}
|
|
|
|
// RegisterMCPRoutes registers MCP HTTP endpoints
|
|
func RegisterMCPRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("/.well-known/oauth-protected-resource", handleOAuthProtectedResource)
|
|
mux.HandleFunc("/.well-known/oauth-authorization-server", handleOAuthAuthorizationServer)
|
|
mux.HandleFunc("/register", handleDynamicClientRegistration)
|
|
mux.HandleFunc("/mcp", handleMCP)
|
|
}
|