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:
James 2026-02-10 03:17:59 -05:00
parent e1b40ab872
commit 6546167d67
2 changed files with 132 additions and 117 deletions

View File

@ -503,12 +503,12 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
return
}
// Use accessToken for internal API calls (dossierID available if needed)
_ = dossierID
// dossierID = authenticated user's ID (used for RBAC in all lib calls)
// accessToken = kept only for image/journal API calls
switch params.Name {
case "list_dossiers":
result, err := mcpListDossiers(accessToken)
result, err := mcpListDossiers(dossierID)
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
return
@ -521,7 +521,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
sendMCPError(w, req.ID, -32602, "dossier required")
return
}
result, err := mcpListStudies(accessToken, dossier)
result, err := mcpListStudies(dossierID, dossier)
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
return
@ -535,7 +535,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
sendMCPError(w, req.ID, -32602, "dossier and study required")
return
}
result, err := mcpListSeries(accessToken, dossier, study)
result, err := mcpListSeries(dossierID, dossier, study)
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
return
@ -549,7 +549,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
sendMCPError(w, req.ID, -32602, "dossier and series required")
return
}
result, err := mcpListSlices(accessToken, dossier, series)
result, err := mcpListSlices(dossierID, dossier, series)
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
return
@ -601,7 +601,7 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
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))
result, err := mcpQueryEntries(dossierID, dossier, category, typ, searchKey, parent, from, to, int(limit))
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
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",
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 {
fmt.Printf("[MCP] query_genome error: %v\n", err)
sendMCPError(w, req.ID, -32000, err.Error())

View File

@ -16,10 +16,12 @@ import (
)
// 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) {
u := apiBaseURL + path
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)
return nil, err
}
// Use the access token for auth
req.Header.Set("Authorization", "Bearer "+accessToken)
client := &http.Client{}
@ -66,51 +67,146 @@ func mcpAPICall(accessToken, path string, params map[string]string) ([]byte, err
return body, nil
}
func mcpListDossiers(accessToken string) (string, error) {
body, err := mcpAPICall(accessToken, "/api/v1/dossiers", nil)
// --- Data query tools: all go through lib with RBAC ---
func mcpListDossiers(accessorID string) (string, error) {
ctx := &lib.AccessContext{AccessorID: accessorID}
dossiers, err := lib.DossierListAccessible(ctx)
if err != nil {
return "", err
}
// Pretty print JSON
var data interface{}
json.Unmarshal(body, &data)
pretty, _ := json.MarshalIndent(data, "", " ")
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(accessToken, dossier string) (string, error) {
body, err := mcpAPICall(accessToken, "/api/v1/dossiers/"+dossier+"/entries", map[string]string{"category": "imaging"})
func mcpListStudies(accessorID, dossier string) (string, error) {
entries, err := lib.EntryList(accessorID, "", lib.CategoryImaging, &lib.EntryFilter{DossierID: dossier})
if err != nil {
return "", err
}
var data interface{}
json.Unmarshal(body, &data)
pretty, _ := json.MarshalIndent(data, "", " ")
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 mcpListSeries(accessToken, dossier, study string) (string, error) {
body, err := mcpAPICall(accessToken, "/api/v1/dossiers/"+dossier+"/entries", map[string]string{"parent": study})
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
}
var data interface{}
json.Unmarshal(body, &data)
pretty, _ := json.MarshalIndent(data, "", " ")
pretty, _ := json.MarshalIndent(result, "", " ")
return string(pretty), nil
}
func mcpListSlices(accessToken, dossier, series string) (string, error) {
body, err := mcpAPICall(accessToken, "/api/v1/dossiers/"+dossier+"/entries", map[string]string{"parent": series})
if err != nil {
return "", err
// 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,
}
var data interface{}
json.Unmarshal(body, &data)
pretty, _ := json.MarshalIndent(data, "", " ")
return string(pretty), nil
result = append(result, entry)
}
pretty, _ := json.MarshalIndent(result, "", " ")
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) {
params := map[string]string{}
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
}
func mcpQueryEntries(accessToken, dossier, category, typ, searchKey, parent, from, to string, limit int) (string, error) {
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
// --- 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}