fix: route all MCP data queries through lib RBAC, kill API roundtrip
list_dossiers, list_studies, list_series, list_slices, query_entries,
get_categories, query_genome — all now call lib directly with
AccessContext{AccessorID: dossierID}. No more HTTP roundtrip to the
internal API with its separate auth path.
Image and journal tools still use API (image rendering logic lives
there, and the API already enforces RBAC via lib.CheckAccess).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e1b40ab872
commit
6546167d67
|
|
@ -503,12 +503,12 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use accessToken for internal API calls (dossierID available if needed)
|
// dossierID = authenticated user's ID (used for RBAC in all lib calls)
|
||||||
_ = dossierID
|
// accessToken = kept only for image/journal API calls
|
||||||
|
|
||||||
switch params.Name {
|
switch params.Name {
|
||||||
case "list_dossiers":
|
case "list_dossiers":
|
||||||
result, err := mcpListDossiers(accessToken)
|
result, err := mcpListDossiers(dossierID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendMCPError(w, req.ID, -32000, err.Error())
|
sendMCPError(w, req.ID, -32000, err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -521,7 +521,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
|
||||||
sendMCPError(w, req.ID, -32602, "dossier required")
|
sendMCPError(w, req.ID, -32602, "dossier required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := mcpListStudies(accessToken, dossier)
|
result, err := mcpListStudies(dossierID, dossier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendMCPError(w, req.ID, -32000, err.Error())
|
sendMCPError(w, req.ID, -32000, err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -535,7 +535,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
|
||||||
sendMCPError(w, req.ID, -32602, "dossier and study required")
|
sendMCPError(w, req.ID, -32602, "dossier and study required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := mcpListSeries(accessToken, dossier, study)
|
result, err := mcpListSeries(dossierID, dossier, study)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendMCPError(w, req.ID, -32000, err.Error())
|
sendMCPError(w, req.ID, -32000, err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -549,7 +549,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
|
||||||
sendMCPError(w, req.ID, -32602, "dossier and series required")
|
sendMCPError(w, req.ID, -32602, "dossier and series required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
result, err := mcpListSlices(accessToken, dossier, series)
|
result, err := mcpListSlices(dossierID, dossier, series)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendMCPError(w, req.ID, -32000, err.Error())
|
sendMCPError(w, req.ID, -32000, err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -601,7 +601,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
|
||||||
from, _ := params.Arguments["from"].(string)
|
from, _ := params.Arguments["from"].(string)
|
||||||
to, _ := params.Arguments["to"].(string)
|
to, _ := params.Arguments["to"].(string)
|
||||||
limit, _ := params.Arguments["limit"].(float64)
|
limit, _ := params.Arguments["limit"].(float64)
|
||||||
result, err := mcpQueryEntries(accessToken, dossier, category, typ, searchKey, parent, from, to, int(limit))
|
result, err := mcpQueryEntries(dossierID, dossier, category, typ, searchKey, parent, from, to, int(limit))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sendMCPError(w, req.ID, -32000, err.Error())
|
sendMCPError(w, req.ID, -32000, err.Error())
|
||||||
return
|
return
|
||||||
|
|
@ -642,7 +642,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
|
||||||
fmt.Printf("[MCP] query_genome: dossier=%s gene=%s category=%s repute=%s limit=%d offset=%d\n",
|
fmt.Printf("[MCP] query_genome: dossier=%s gene=%s category=%s repute=%s limit=%d offset=%d\n",
|
||||||
dossier, gene, category, repute, limit, offset)
|
dossier, gene, category, repute, limit, offset)
|
||||||
|
|
||||||
result, err := mcpQueryGenome(accessToken, dossier, dossierID, gene, search, category, rsids, minMag, repute, includeHidden, limit, offset)
|
result, err := mcpQueryGenome(dossierID, dossier, gene, search, category, rsids, minMag, repute, includeHidden, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("[MCP] query_genome error: %v\n", err)
|
fmt.Printf("[MCP] query_genome error: %v\n", err)
|
||||||
sendMCPError(w, req.ID, -32000, err.Error())
|
sendMCPError(w, req.ID, -32000, err.Error())
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// MCP Tool Implementations
|
// MCP Tool Implementations
|
||||||
// These call the internal API endpoints to get data
|
// 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
|
const apiBaseURL = "http://localhost:8082" // Internal API server (images only)
|
||||||
|
|
||||||
|
// mcpAPICall is used ONLY for image endpoints that require server-side rendering.
|
||||||
func mcpAPICall(accessToken, path string, params map[string]string) ([]byte, error) {
|
func mcpAPICall(accessToken, path string, params map[string]string) ([]byte, error) {
|
||||||
u := apiBaseURL + path
|
u := apiBaseURL + path
|
||||||
if params != nil && len(params) > 0 {
|
if params != nil && len(params) > 0 {
|
||||||
|
|
@ -39,7 +41,6 @@ func mcpAPICall(accessToken, path string, params map[string]string) ([]byte, err
|
||||||
log.Printf("[MCP] Request error: %v", err)
|
log.Printf("[MCP] Request error: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Use the access token for auth
|
|
||||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
@ -66,51 +67,146 @@ func mcpAPICall(accessToken, path string, params map[string]string) ([]byte, err
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mcpListDossiers(accessToken string) (string, error) {
|
// --- Data query tools: all go through lib with RBAC ---
|
||||||
body, err := mcpAPICall(accessToken, "/api/v1/dossiers", nil)
|
|
||||||
|
func mcpListDossiers(accessorID string) (string, error) {
|
||||||
|
ctx := &lib.AccessContext{AccessorID: accessorID}
|
||||||
|
dossiers, err := lib.DossierListAccessible(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Pretty print JSON
|
var result []map[string]any
|
||||||
var data interface{}
|
for _, d := range dossiers {
|
||||||
json.Unmarshal(body, &data)
|
result = append(result, map[string]any{
|
||||||
pretty, _ := json.MarshalIndent(data, "", " ")
|
"id": d.DossierID,
|
||||||
|
"name": d.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
pretty, _ := json.MarshalIndent(result, "", " ")
|
||||||
return string(pretty), nil
|
return string(pretty), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mcpListStudies(accessToken, dossier string) (string, error) {
|
func mcpListStudies(accessorID, dossier string) (string, error) {
|
||||||
body, err := mcpAPICall(accessToken, "/api/v1/dossiers/"+dossier+"/entries", map[string]string{"category": "imaging"})
|
entries, err := lib.EntryList(accessorID, "", lib.CategoryImaging, &lib.EntryFilter{DossierID: dossier})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
var data interface{}
|
return formatEntries(entries), nil
|
||||||
json.Unmarshal(body, &data)
|
}
|
||||||
pretty, _ := json.MarshalIndent(data, "", " ")
|
|
||||||
|
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
|
return string(pretty), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mcpListSeries(accessToken, dossier, study string) (string, error) {
|
func mcpQueryGenome(accessorID, dossier, gene, search, category, rsids string, minMag float64, repute string, includeHidden bool, limit, offset int) (string, error) {
|
||||||
body, err := mcpAPICall(accessToken, "/api/v1/dossiers/"+dossier+"/entries", map[string]string{"parent": study})
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
var data interface{}
|
pretty, _ := json.MarshalIndent(result, "", " ")
|
||||||
json.Unmarshal(body, &data)
|
|
||||||
pretty, _ := json.MarshalIndent(data, "", " ")
|
|
||||||
return string(pretty), nil
|
return string(pretty), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mcpListSlices(accessToken, dossier, series string) (string, error) {
|
// formatEntries converts entries to the standard MCP response format.
|
||||||
body, err := mcpAPICall(accessToken, "/api/v1/dossiers/"+dossier+"/entries", map[string]string{"parent": series})
|
func formatEntries(entries []*lib.Entry) string {
|
||||||
if err != nil {
|
var result []map[string]any
|
||||||
return "", err
|
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,
|
||||||
}
|
}
|
||||||
var data interface{}
|
result = append(result, entry)
|
||||||
json.Unmarshal(body, &data)
|
}
|
||||||
pretty, _ := json.MarshalIndent(data, "", " ")
|
pretty, _ := json.MarshalIndent(result, "", " ")
|
||||||
return string(pretty), nil
|
return string(pretty)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Image tools: use API (image rendering lives there, API enforces RBAC via lib) ---
|
||||||
|
|
||||||
func mcpFetchImage(accessToken, dossier, slice string, wc, ww float64) (map[string]interface{}, error) {
|
func mcpFetchImage(accessToken, dossier, slice string, wc, ww float64) (map[string]interface{}, error) {
|
||||||
params := map[string]string{}
|
params := map[string]string{}
|
||||||
if wc != 0 {
|
if wc != 0 {
|
||||||
|
|
@ -147,88 +243,7 @@ func mcpFetchContactSheet(accessToken, dossier, series string, wc, ww float64) (
|
||||||
return mcpImageContent(b64, "image/webp", fmt.Sprintf("Contact sheet %s (%d bytes)", series[:8], len(body))), nil
|
return mcpImageContent(b64, "image/webp", fmt.Sprintf("Contact sheet %s (%d bytes)", series[:8], len(body))), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mcpQueryEntries(accessToken, dossier, category, typ, searchKey, parent, from, to string, limit int) (string, error) {
|
// --- Journal tools (in development — kept as API passthrough) ---
|
||||||
params := map[string]string{}
|
|
||||||
if category != "" {
|
|
||||||
params["category"] = category
|
|
||||||
}
|
|
||||||
if typ != "" {
|
|
||||||
params["type"] = typ
|
|
||||||
}
|
|
||||||
if searchKey != "" {
|
|
||||||
params["search_key"] = searchKey
|
|
||||||
}
|
|
||||||
if parent != "" {
|
|
||||||
params["parent"] = parent
|
|
||||||
}
|
|
||||||
if from != "" {
|
|
||||||
params["from"] = from
|
|
||||||
}
|
|
||||||
if to != "" {
|
|
||||||
params["to"] = to
|
|
||||||
}
|
|
||||||
if limit > 0 {
|
|
||||||
params["limit"] = strconv.Itoa(limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := mcpAPICall(accessToken, "/api/v1/dossiers/"+dossier+"/entries", params)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
var data interface{}
|
|
||||||
json.Unmarshal(body, &data)
|
|
||||||
pretty, _ := json.MarshalIndent(data, "", " ")
|
|
||||||
return string(pretty), 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(accessToken, dossier, accessorID, 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Journal MCP Tools
|
|
||||||
|
|
||||||
func mcpListJournals(accessToken, dossier string, days int, status *int, journalType string) (string, error) {
|
func mcpListJournals(accessToken, dossier string, days int, status *int, journalType string) (string, error) {
|
||||||
params := map[string]string{"dossier": dossier}
|
params := map[string]string{"dossier": dossier}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue