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
|
||||
}
|
||||
|
||||
// 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())
|
||||
|
|
|
|||
|
|
@ -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,50 +67,145 @@ 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{}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue