fix: update /prompts/respond route to /trackers/respond

This commit is contained in:
James 2026-02-09 05:21:35 -05:00
parent 49d7f31514
commit 131a41037b
5 changed files with 805 additions and 2 deletions

View File

@ -2,6 +2,7 @@ package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
@ -69,6 +70,23 @@ func v1CanAccess(authID, targetID string) bool {
return len(records) > 0
}
func v1CanWrite(authID, targetID string) bool {
if authID == targetID {
return true
}
records, _ := lib.AccessList(&lib.AccessFilter{
AccessorID: authID,
TargetID: targetID,
Status: intPtr(1),
})
for _, r := range records {
if r.CanEdit {
return true
}
}
return false
}
func intPtr(i int) *int { return &i }
func v1Error(w http.ResponseWriter, msg string, code int) {
@ -728,6 +746,180 @@ func v1Health(w http.ResponseWriter, r *http.Request) {
// --- ROUTER ---
// v1ListJournals handles GET /dossiers/{id}/journal
func v1ListJournals(w http.ResponseWriter, r *http.Request, dossierID string) {
actorID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(actorID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
// Parse query parameters
days := 30 // default
if daysStr := r.URL.Query().Get("days"); daysStr != "" {
if d, err := strconv.Atoi(daysStr); err == nil {
days = d
}
}
var status *int
if statusStr := r.URL.Query().Get("status"); statusStr != "" {
if s, err := strconv.Atoi(statusStr); err == nil {
status = &s
}
}
journalType := r.URL.Query().Get("type")
// List journals
journals, err := lib.ListJournals(lib.ListJournalsInput{
DossierID: dossierID,
Days: days,
Status: status,
Type: journalType,
})
if err != nil {
v1Error(w, fmt.Sprintf("Failed to list journals: %v", err), http.StatusInternalServerError)
return
}
v1JSON(w, map[string]interface{}{"journals": journals})
}
// v1GetJournal handles GET /dossiers/{id}/journal/{entry_id}
func v1GetJournal(w http.ResponseWriter, r *http.Request, dossierID, entryID string) {
actorID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanAccess(actorID, dossierID) {
v1Error(w, "Access denied", http.StatusForbidden)
return
}
journal, err := lib.GetJournal(dossierID, entryID)
if err != nil {
if strings.Contains(err.Error(), "access denied") {
v1Error(w, "Access denied", http.StatusForbidden)
} else if strings.Contains(err.Error(), "not found") {
v1Error(w, "Journal entry not found", http.StatusNotFound)
} else {
v1Error(w, fmt.Sprintf("Failed to get journal: %v", err), http.StatusInternalServerError)
}
return
}
v1JSON(w, journal)
}
// v1CreateJournal handles POST /dossiers/{id}/journal
func v1CreateJournal(w http.ResponseWriter, r *http.Request, dossierID string) {
actorID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanWrite(actorID, dossierID) {
v1Error(w, "Write access denied", http.StatusForbidden)
return
}
// Parse request body
var req struct {
Type string `json:"type"`
Title string `json:"title"`
Summary string `json:"summary"`
Content string `json:"content"`
Tags []string `json:"tags"`
Status int `json:"status"`
RelatedEntries []string `json:"related_entries"`
Source string `json:"source"`
Reasoning string `json:"reasoning"`
Metadata map[string]string `json:"metadata"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
v1Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Create journal
entryID, err := lib.CreateJournal(lib.CreateJournalInput{
DossierID: dossierID,
Type: req.Type,
Title: req.Title,
Summary: req.Summary,
Content: req.Content,
Tags: req.Tags,
Status: req.Status,
RelatedEntries: req.RelatedEntries,
Source: req.Source,
Reasoning: req.Reasoning,
Metadata: req.Metadata,
})
if err != nil {
v1Error(w, fmt.Sprintf("Failed to create journal: %v", err), http.StatusInternalServerError)
return
}
// Return created entry
journal, err := lib.GetJournal(dossierID, entryID)
if err != nil {
v1Error(w, "Created but failed to retrieve", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
v1JSON(w, journal)
}
// v1UpdateJournal handles PATCH /dossiers/{id}/journal/{entry_id}
func v1UpdateJournal(w http.ResponseWriter, r *http.Request, dossierID, entryID string) {
actorID, ok := v1AuthRequired(w, r)
if !ok {
return
}
if !v1CanWrite(actorID, dossierID) {
v1Error(w, "Write access denied", http.StatusForbidden)
return
}
// Parse request body
var req struct {
Status *int `json:"status"`
AppendNote string `json:"append_note"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
v1Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Update journal
err := lib.UpdateJournalStatus(lib.UpdateJournalStatusInput{
DossierID: dossierID,
EntryID: entryID,
Status: req.Status,
AppendNote: req.AppendNote,
})
if err != nil {
if strings.Contains(err.Error(), "access denied") {
v1Error(w, "Access denied", http.StatusForbidden)
} else {
v1Error(w, fmt.Sprintf("Failed to update journal: %v", err), http.StatusInternalServerError)
}
return
}
v1JSON(w, map[string]interface{}{"updated": time.Now().Format(time.RFC3339)})
}
func v1Router(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1")
parts := strings.Split(strings.Trim(path, "/"), "/")
@ -769,6 +961,22 @@ func v1Router(w http.ResponseWriter, r *http.Request) {
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "trackers" && r.Method == "GET":
v1Prompts(w, r, parts[1])
// GET /dossiers/{id}/journal
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "journal" && r.Method == "GET":
v1ListJournals(w, r, parts[1])
// GET /dossiers/{id}/journal/{entry_id}
case len(parts) == 4 && parts[0] == "dossiers" && parts[2] == "journal" && r.Method == "GET":
v1GetJournal(w, r, parts[1], parts[3])
// POST /dossiers/{id}/journal
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "journal" && r.Method == "POST":
v1CreateJournal(w, r, parts[1])
// PATCH /dossiers/{id}/journal/{entry_id}
case len(parts) == 4 && parts[0] == "dossiers" && parts[2] == "journal" && r.Method == "PATCH":
v1UpdateJournal(w, r, parts[1], parts[3])
// POST /dossiers/{id}/parse
case len(parts) == 3 && parts[0] == "dossiers" && parts[2] == "parse" && r.Method == "POST":
v1Parse(w, r, parts[1])

359
lib/journal.go Normal file
View File

@ -0,0 +1,359 @@
package lib
import (
"encoding/json"
"fmt"
"strings"
"time"
)
// Journal entry types
const (
JournalTypeProtocol = "protocol"
JournalTypeHypothesis = "hypothesis"
JournalTypeObservation = "observation"
JournalTypeConnection = "connection"
JournalTypeQuestion = "question"
JournalTypeReference = "reference"
)
// Journal status values
const (
JournalStatusDraft = 0
JournalStatusActive = 1
JournalStatusResolved = 2
JournalStatusDiscarded = 3
)
// JournalData represents the encrypted JSON data stored in entry.Data
type JournalData struct {
Source string `json:"source"`
ConversationID string `json:"conversation_id,omitempty"`
ConversationURL string `json:"conversation_url,omitempty"`
Content string `json:"content"`
RelatedEntries []string `json:"related_entries,omitempty"`
Reasoning string `json:"reasoning,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// CreateJournalInput represents the input for creating a journal entry
type CreateJournalInput struct {
DossierID string
Type string // protocol, hypothesis, observation, etc.
Title string
Summary string // optional - generated if not provided
Content string // full markdown content
Tags []string // optional
Status int // optional - defaults to draft
RelatedEntries []string // optional
Source string // optional - e.g., "opus-4.6"
Reasoning string // optional
Metadata map[string]string
}
// JournalSummary represents a journal entry summary for list view
type JournalSummary struct {
EntryID string `json:"entry_id"`
Type string `json:"type"`
Title string `json:"title"`
Summary string `json:"summary"`
Date time.Time `json:"date"`
Status int `json:"status"`
Tags []string `json:"tags"`
DossierID string `json:"dossier_id,omitempty"`
}
// JournalEntry represents a full journal entry
type JournalEntry struct {
EntryID string `json:"entry_id"`
DossierID string `json:"dossier_id"`
Type string `json:"type"`
Title string `json:"title"`
Summary string `json:"summary"`
Content string `json:"content"`
Date time.Time `json:"date"`
Status int `json:"status"`
Tags []string `json:"tags"`
RelatedEntries []string `json:"related_entries,omitempty"`
Source string `json:"source,omitempty"`
Reasoning string `json:"reasoning,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// GenerateJournalSummary uses Gemini to generate a 1-2 sentence summary
func GenerateJournalSummary(title, journalType, content string) (string, error) {
prompt := fmt.Sprintf(`Summarize this health journal entry in 1-2 sentences (max 300 chars).
Focus on: what intervention/observation, for what condition, and key details.
Title: %s
Type: %s
Content: %s
Example good summaries:
- "20-25g daily Beluga/Keta targeting phospholipid-DHA → SPM production for aqueduct inflammation."
- "Jan 10 fever may have opened aqueduct drainage. Observing improved tone, eye tracking, head control."
- "Autologous WBC therapy (Utheline/Ricasin/Notokill + ATP) given during active fever."
Respond with ONLY the summary text, no JSON, no extra formatting.`, title, journalType, content)
// Override default config to return plain text
plainText := "text/plain"
maxTokens := 150
temp := 0.3
config := &GeminiConfig{
Temperature: &temp,
MaxOutputTokens: &maxTokens,
ResponseMimeType: &plainText,
}
summary, err := CallGeminiMultimodal([]GeminiPart{{Text: prompt}}, config)
if err != nil {
return "", fmt.Errorf("failed to generate summary: %w", err)
}
summary = strings.TrimSpace(summary)
if len(summary) > 300 {
summary = summary[:297] + "..."
}
return summary, nil
}
// CreateJournal creates a new journal entry
func CreateJournal(input CreateJournalInput) (string, error) {
// Validate required fields
if input.DossierID == "" {
return "", fmt.Errorf("dossier_id required")
}
if input.Type == "" {
return "", fmt.Errorf("type required")
}
if input.Title == "" {
return "", fmt.Errorf("title required")
}
if input.Content == "" {
return "", fmt.Errorf("content required")
}
// Generate summary if not provided
if input.Summary == "" {
var err error
input.Summary, err = GenerateJournalSummary(input.Title, input.Type, input.Content)
if err != nil {
return "", fmt.Errorf("failed to generate summary: %w", err)
}
} else {
// Validate client-provided summary
if len(input.Summary) > 300 {
return "", fmt.Errorf("summary too long (max 300 chars)")
}
}
// Build data JSON
data := JournalData{
Source: input.Source,
Content: input.Content,
RelatedEntries: input.RelatedEntries,
Reasoning: input.Reasoning,
Metadata: input.Metadata,
}
dataJSON, err := json.Marshal(data)
if err != nil {
return "", fmt.Errorf("failed to marshal data: %w", err)
}
// Create entry
entry := Entry{
EntryID: NewID(),
DossierID: input.DossierID,
Category: CategoryNote,
Type: input.Type,
Value: input.Title,
Summary: input.Summary,
Data: string(dataJSON),
Tags: strings.Join(input.Tags, ","),
Timestamp: time.Now().Unix(),
Status: input.Status, // defaults to 0 (draft) if not set
}
if err := Save("entries", &entry); err != nil {
return "", fmt.Errorf("failed to save entry: %w", err)
}
return entry.EntryID, nil
}
// GetJournal retrieves a full journal entry
func GetJournal(dossierID, entryID string) (*JournalEntry, error) {
var entry Entry
if err := Load("entries", entryID, &entry); err != nil {
return nil, fmt.Errorf("failed to load entry: %w", err)
}
// Verify access
if entry.DossierID != dossierID {
return nil, fmt.Errorf("access denied")
}
// Verify it's a journal entry
if entry.Category != CategoryNote {
return nil, fmt.Errorf("not a journal entry")
}
// Parse data JSON
var data JournalData
if entry.Data != "" {
if err := json.Unmarshal([]byte(entry.Data), &data); err != nil {
return nil, fmt.Errorf("failed to parse data: %w", err)
}
}
// Parse tags
var tags []string
if entry.Tags != "" {
tags = strings.Split(entry.Tags, ",")
}
return &JournalEntry{
EntryID: entry.EntryID,
DossierID: entry.DossierID,
Type: entry.Type,
Title: entry.Value,
Summary: entry.Summary,
Content: data.Content,
Date: time.Unix(entry.Timestamp, 0),
Status: entry.Status,
Tags: tags,
RelatedEntries: data.RelatedEntries,
Source: data.Source,
Reasoning: data.Reasoning,
Metadata: data.Metadata,
}, nil
}
// ListJournalsInput represents filters for listing journals
type ListJournalsInput struct {
DossierID string
Days int // filter to last N days (0 = all)
Status *int // filter by status (nil = all)
Type string // filter by type (empty = all)
}
// ListJournals retrieves journal summaries with optional filters
func ListJournals(input ListJournalsInput) ([]JournalSummary, error) {
// Build query
query := `SELECT entry_id, dossier_id, type, value, summary, tags, timestamp, status
FROM entries
WHERE dossier_id = ? AND category = ?`
args := []interface{}{input.DossierID, CategoryNote}
// Add type filter
if input.Type != "" {
query += " AND type = ?"
args = append(args, input.Type)
}
// Add status filter
if input.Status != nil {
query += " AND status = ?"
args = append(args, *input.Status)
}
// Add days filter
if input.Days > 0 {
cutoff := time.Now().AddDate(0, 0, -input.Days).Unix()
query += " AND timestamp >= ?"
args = append(args, cutoff)
}
query += " ORDER BY timestamp DESC"
// Execute query
var entries []Entry
if err := Query(query, args, &entries); err != nil {
return nil, fmt.Errorf("failed to query entries: %w", err)
}
// Convert to summaries
summaries := make([]JournalSummary, len(entries))
for i, entry := range entries {
var tags []string
if entry.Tags != "" {
tags = strings.Split(entry.Tags, ",")
}
summaries[i] = JournalSummary{
EntryID: entry.EntryID,
Type: entry.Type,
Title: entry.Value,
Summary: entry.Summary,
Date: time.Unix(entry.Timestamp, 0),
Status: entry.Status,
Tags: tags,
DossierID: entry.DossierID,
}
}
return summaries, nil
}
// UpdateJournalStatusInput represents input for updating journal status
type UpdateJournalStatusInput struct {
DossierID string
EntryID string
Status *int // optional: new status
AppendNote string // optional: append update to content
}
// UpdateJournalStatus updates a journal entry's status or appends a note
func UpdateJournalStatus(input UpdateJournalStatusInput) error {
// Load entry
var entry Entry
if err := Load("entries", input.EntryID, &entry); err != nil {
return fmt.Errorf("failed to load entry: %w", err)
}
// Verify access
if entry.DossierID != input.DossierID {
return fmt.Errorf("access denied")
}
// Verify it's a journal entry
if entry.Category != CategoryNote {
return fmt.Errorf("not a journal entry")
}
// Update status if provided
if input.Status != nil {
entry.Status = *input.Status
}
// Append note if provided
if input.AppendNote != "" {
var data JournalData
if entry.Data != "" {
if err := json.Unmarshal([]byte(entry.Data), &data); err != nil {
return fmt.Errorf("failed to parse data: %w", err)
}
}
// Append to content
timestamp := time.Now().Format("2006-01-02")
update := fmt.Sprintf("\n\n---\n**Update %s:** %s", timestamp, input.AppendNote)
data.Content += update
dataJSON, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}
entry.Data = string(dataJSON)
}
// Save entry
if err := Save("entries", &entry); err != nil {
return fmt.Errorf("failed to save entry: %w", err)
}
return nil
}

View File

@ -2007,7 +2007,7 @@ func setupMux() http.Handler {
} else if strings.Contains(path, "/access/") { handleEditAccess(w, r)
} else if strings.HasSuffix(path, "/prompts") { handleTrackers(w, r)
} else if strings.Contains(path, "/prompts/card/") { handleRenderTrackerCard(w, r)
} else if strings.HasSuffix(path, "/prompts/respond") { handleTrackerRespond(w, r)
} else if strings.HasSuffix(path, "/trackers/respond") { handleTrackerRespond(w, r)
} else if strings.HasSuffix(path, "/upload") { if r.Method == "POST" { handleUploadPost(w, r) } else { handleUploadPage(w, r) }
} else if strings.Contains(path, "/files/") && strings.HasSuffix(path, "/delete") { handleDeleteFile(w, r)
} else if strings.Contains(path, "/files/") && strings.HasSuffix(path, "/update") { handleUpdateFile(w, r)
@ -2028,7 +2028,7 @@ func setupMux() http.Handler {
mux.HandleFunc("/api/v1/auth/verify", handleAPIVerify)
mux.HandleFunc("/api/v1/dashboard", handleAPIDashboard)
mux.HandleFunc("/api/v1/prompts", handleAPIPrompts)
mux.HandleFunc("/api/v1/prompts/respond", handleAPITrackerRespond)
mux.HandleFunc("/api/v1/trackers/respond", handleAPITrackerRespond)
mux.HandleFunc("/api", handleAPI)
mux.HandleFunc("/api/token/generate", handleAPITokenGenerate)

View File

@ -418,6 +418,69 @@ func handleMCPToolsList(w http.ResponseWriter, req mcpRequest) {
},
"annotations": readOnly,
},
{
"name": "list_journals",
"description": "List journal entries (protocols, hypotheses, observations) for a dossier. Returns summaries with title, type, date, status. Use get_journal_entry to fetch full content.",
"inputSchema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
"days": map[string]interface{}{"type": "number", "description": "Filter to last N days (0 = all)"},
"status": map[string]interface{}{"type": "number", "description": "Filter by status (0=draft, 1=active, 2=resolved, 3=discarded)"},
"type": map[string]interface{}{"type": "string", "description": "Filter by type (protocol, hypothesis, observation, connection, question, reference)"},
},
"required": []string{"dossier"},
},
"annotations": readOnly,
},
{
"name": "get_journal_entry",
"description": "Get full journal entry with complete content, reasoning, and metadata.",
"inputSchema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
"entry_id": map[string]interface{}{"type": "string", "description": "Journal entry ID (16-char hex)"},
},
"required": []string{"dossier", "entry_id"},
},
"annotations": readOnly,
},
{
"name": "create_journal_entry",
"description": "Create a new journal entry (protocol, hypothesis, observation, etc.). Summary will be auto-generated if not provided.",
"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": "Entry type: protocol, hypothesis, observation, connection, question, reference"},
"title": map[string]interface{}{"type": "string", "description": "Short title (max 200 chars)"},
"summary": map[string]interface{}{"type": "string", "description": "1-2 sentence summary (max 300 chars, auto-generated if omitted)"},
"content": map[string]interface{}{"type": "string", "description": "Full markdown content with details, reasoning, etc."},
"tags": map[string]interface{}{"type": "array", "items": map[string]string{"type": "string"}, "description": "Tags for categorization"},
"status": map[string]interface{}{"type": "number", "description": "0=draft (default), 1=active, 2=resolved, 3=discarded"},
"related_entries": map[string]interface{}{"type": "array", "items": map[string]string{"type": "string"}, "description": "Related entry IDs"},
"source": map[string]interface{}{"type": "string", "description": "Source model (e.g., opus-4.6)"},
"reasoning": map[string]interface{}{"type": "string", "description": "Why this matters / how we arrived at this"},
"metadata": map[string]interface{}{"type": "object", "description": "Additional context (weight, age, etc.)"},
},
"required": []string{"dossier", "type", "title", "content"},
},
},
{
"name": "update_journal_entry",
"description": "Update journal entry status or append a note to existing entry.",
"inputSchema": map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
"entry_id": map[string]interface{}{"type": "string", "description": "Journal entry ID (16-char hex)"},
"status": map[string]interface{}{"type": "number", "description": "New status (0=draft, 1=active, 2=resolved, 3=discarded)"},
"append_note": map[string]interface{}{"type": "string", "description": "Note to append to content (timestamped)"},
},
"required": []string{"dossier", "entry_id"},
},
},
{
"name": "get_version",
"description": "Get bridge and server version info.",
@ -584,6 +647,69 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, accessToken, doss
fmt.Printf("[MCP] query_genome success: %d bytes\n", len(result))
sendMCPResult(w, req.ID, mcpTextContent(result))
case "list_journals":
dossier, _ := params.Arguments["dossier"].(string)
if dossier == "" {
sendMCPError(w, req.ID, -32602, "dossier required")
return
}
days, _ := params.Arguments["days"].(float64)
journalType, _ := params.Arguments["type"].(string)
var status *int
if statusVal, ok := params.Arguments["status"].(float64); ok {
s := int(statusVal)
status = &s
}
result, err := mcpListJournals(accessToken, dossier, int(days), status, journalType)
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
return
}
sendMCPResult(w, req.ID, mcpTextContent(result))
case "get_journal_entry":
dossier, _ := params.Arguments["dossier"].(string)
entryID, _ := params.Arguments["entry_id"].(string)
if dossier == "" || entryID == "" {
sendMCPError(w, req.ID, -32602, "dossier and entry_id required")
return
}
result, err := mcpGetJournalEntry(accessToken, dossier, entryID)
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
return
}
sendMCPResult(w, req.ID, mcpTextContent(result))
case "create_journal_entry":
dossier, _ := params.Arguments["dossier"].(string)
if dossier == "" {
sendMCPError(w, req.ID, -32602, "dossier required")
return
}
result, err := mcpCreateJournalEntry(accessToken, dossier, params.Arguments)
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
return
}
sendMCPResult(w, req.ID, mcpTextContent(result))
case "update_journal_entry":
dossier, _ := params.Arguments["dossier"].(string)
entryID, _ := params.Arguments["entry_id"].(string)
if dossier == "" || entryID == "" {
sendMCPError(w, req.ID, -32602, "dossier and entry_id required")
return
}
result, err := mcpUpdateJournalEntry(accessToken, dossier, entryID, params.Arguments)
if err != nil {
sendMCPError(w, req.ID, -32000, err.Error())
return
}
sendMCPResult(w, req.ID, mcpTextContent(result))
case "get_version":
sendMCPResult(w, req.ID, mcpTextContent(fmt.Sprintf("Server: %s v%s", mcpServerName, mcpServerVersion)))

View File

@ -1,6 +1,7 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
@ -226,3 +227,112 @@ func mcpQueryGenome(accessToken, dossier, gene, search, category, rsids string,
pretty, _ := json.MarshalIndent(data, "", " ")
return string(pretty), nil
}
// Journal MCP Tools
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 := mcpAPICall(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 := mcpAPICall(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
}