package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "inou/lib" ) // MCP Tool Implementations // Data queries go through lib directly with RBAC enforcement. // Image rendering goes through the API (which also enforces RBAC via lib). const apiBaseURL = "http://localhost:8082" // Internal API server (images only) // mcpAPIGet calls the internal API with Bearer auth. func mcpAPIGet(accessToken, path string, params map[string]string) ([]byte, error) { v := url.Values{} for k, val := range params { if val != "" { v.Set(k, val) } } u := apiBaseURL + path if len(v) > 0 { u += "?" + v.Encode() } req, err := http.NewRequest("GET", u, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != 200 { return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) } return body, nil } // --- Data query tools: all go through lib with RBAC --- func mcpListDossiers(accessorID string) (string, error) { dossiers, err := lib.DossierQuery(accessorID) if err != nil { return "", err } var result []map[string]any for _, d := range dossiers { result = append(result, map[string]any{ "id": d.DossierID, "name": d.Name, }) } pretty, _ := json.MarshalIndent(result, "", " ") return string(pretty), nil } func mcpListStudies(accessorID, dossier string) (string, error) { entries, err := lib.EntryList(accessorID, "", lib.CategoryImaging, &lib.EntryFilter{DossierID: dossier}) if err != nil { return "", err } return formatEntries(entries), nil } func mcpListSeries(accessorID, dossier, study string) (string, error) { entries, err := lib.EntryList(accessorID, study, 0, &lib.EntryFilter{DossierID: dossier}) if err != nil { return "", err } return formatEntries(entries), nil } func mcpListSlices(accessorID, dossier, series string) (string, error) { entries, err := lib.EntryList(accessorID, series, 0, &lib.EntryFilter{DossierID: dossier}) if err != nil { return "", err } return formatEntries(entries), nil } func mcpQueryEntries(accessorID, dossier, category, typ, searchKey, parent, from, to string, limit int) (string, error) { cat := 0 if category != "" { cat = lib.CategoryFromString[category] } filter := &lib.EntryFilter{DossierID: dossier} if typ != "" { filter.Type = typ } if searchKey != "" { filter.SearchKey = searchKey } if from != "" { filter.FromDate, _ = strconv.ParseInt(from, 10, 64) } if to != "" { filter.ToDate, _ = strconv.ParseInt(to, 10, 64) } if limit > 0 { filter.Limit = limit } entries, err := lib.EntryList(accessorID, parent, cat, filter) if err != nil { return "", err } return formatEntries(entries), nil } func mcpGetCategories(dossier, accessorID string) (string, error) { ctx := &lib.AccessContext{AccessorID: accessorID} result, err := lib.EntryCategoryCounts(ctx, dossier) if err != nil { return "", err } pretty, _ := json.MarshalIndent(result, "", " ") return string(pretty), nil } func mcpQueryGenome(accessorID, dossier, gene, search, category, rsids string, minMag float64, repute string, includeHidden bool, limit, offset int) (string, error) { ctx := &lib.AccessContext{AccessorID: accessorID} var rsidList []string if rsids != "" { rsidList = strings.Split(rsids, ",") } if limit <= 0 { numTerms := 1 if gene != "" { numTerms = len(strings.Split(gene, ",")) } if len(rsidList) > numTerms { numTerms = len(rsidList) } limit = 20 * numTerms } result, err := lib.GenomeQuery(ctx, dossier, lib.GenomeQueryOpts{ Category: category, Search: search, Gene: gene, RSIDs: rsidList, MinMagnitude: minMag, Repute: repute, IncludeHidden: includeHidden, Limit: limit, Offset: offset, AccessorID: accessorID, }) if err != nil { return "", err } pretty, _ := json.MarshalIndent(result, "", " ") return string(pretty), nil } // formatEntries converts entries to the standard MCP response format. func formatEntries(entries []*lib.Entry) string { var result []map[string]any for _, e := range entries { entry := map[string]any{ "id": e.EntryID, "parent_id": e.ParentID, "category": lib.CategoryName(e.Category), "type": e.Type, "summary": e.Summary, "ordinal": e.Ordinal, "timestamp": e.Timestamp, } result = append(result, entry) } pretty, _ := json.MarshalIndent(result, "", " ") return string(pretty) } // --- Image tools: RBAC via lib, then API for rendering --- func mcpFetchImage(accessToken, dossier, slice string, wc, ww float64) (map[string]interface{}, error) { params := map[string]string{} if wc != 0 { params["wc"] = strconv.FormatFloat(wc, 'f', 0, 64) } if ww != 0 { params["ww"] = strconv.FormatFloat(ww, 'f', 0, 64) } body, err := mcpAPIGet(accessToken, "/image/"+slice, params) if err != nil { return nil, err } b64 := base64.StdEncoding.EncodeToString(body) return mcpImageContent(b64, "image/webp", fmt.Sprintf("Slice %s (%d bytes)", slice[:8], len(body))), nil } func mcpFetchContactSheet(accessToken, dossier, series string, wc, ww float64) (map[string]interface{}, error) { params := map[string]string{} if wc != 0 { params["wc"] = strconv.FormatFloat(wc, 'f', 0, 64) } if ww != 0 { params["ww"] = strconv.FormatFloat(ww, 'f', 0, 64) } body, err := mcpAPIGet(accessToken, "/contact-sheet.webp/"+series, params) if err != nil { return nil, err } b64 := base64.StdEncoding.EncodeToString(body) return mcpImageContent(b64, "image/webp", fmt.Sprintf("Contact sheet %s (%d bytes)", series[:8], len(body))), nil } // --- Journal tools (in development — kept as API passthrough) --- func mcpListJournals(accessToken, dossier string, days int, status *int, journalType string) (string, error) { params := map[string]string{"dossier": dossier} if days > 0 { params["days"] = strconv.Itoa(days) } if status != nil { params["status"] = strconv.Itoa(*status) } if journalType != "" { params["type"] = journalType } body, err := mcpAPIGet(accessToken, "/api/v1/dossiers/"+dossier+"/journal", params) if err != nil { return "", err } var data interface{} json.Unmarshal(body, &data) pretty, _ := json.MarshalIndent(data, "", " ") return string(pretty), nil } func mcpGetJournalEntry(accessToken, dossier, entryID string) (string, error) { body, err := mcpAPIGet(accessToken, "/api/v1/dossiers/"+dossier+"/journal/"+entryID, nil) if err != nil { return "", err } var data interface{} json.Unmarshal(body, &data) pretty, _ := json.MarshalIndent(data, "", " ") return string(pretty), nil } func mcpCreateJournalEntry(accessToken, dossier string, params map[string]interface{}) (string, error) { u := apiBaseURL + "/api/v1/dossiers/" + dossier + "/journal" jsonData, err := json.Marshal(params) if err != nil { return "", fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequest("POST", u, io.NopCloser(bytes.NewReader(jsonData))) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", err } if resp.StatusCode != 200 && resp.StatusCode != 201 { return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) } var data interface{} json.Unmarshal(body, &data) pretty, _ := json.MarshalIndent(data, "", " ") return string(pretty), nil } func mcpUpdateJournalEntry(accessToken, dossier, entryID string, params map[string]interface{}) (string, error) { u := apiBaseURL + "/api/v1/dossiers/" + dossier + "/journal/" + entryID jsonData, err := json.Marshal(params) if err != nil { return "", fmt.Errorf("failed to marshal request: %w", err) } req, err := http.NewRequest("PATCH", u, io.NopCloser(bytes.NewReader(jsonData))) if err != nil { return "", err } req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", err } if resp.StatusCode != 200 { return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) } var data interface{} json.Unmarshal(body, &data) pretty, _ := json.MarshalIndent(data, "", " ") return string(pretty), nil }