feat: add create_entry MCP tool for direct AI-to-inou writing
Adds a write-capable create_entry tool to the MCP server, enabling Claude (Opus/Sonnet/Haiku) to save health insights directly into a dossier instead of losing them in chat. Tool: create_entry - Required: dossier (16-char hex), category (e.g. 'supplement', 'nutrition', 'tracker'), value (short label) - Optional: type, summary, data (JSON), parent, timestamp (default: now) - RBAC-enforced via EntryWrite — only writable dossiers accepted - Category validated against CategoryFromString; unknown categories return a helpful error listing valid options - data field validated as JSON before write Use cases: - Claude analyzing supplements: 'Create a supplement entry for Vitamin D3 5000 IU with notes on dosing rationale' - Nutrition logs from a meal conversation - Tracker observations or AI-generated insights with provenance The MCP server was previously read-only (all tools had readOnlyHint). create_entry intentionally omits readOnlyHint as it mutates state.
This commit is contained in:
parent
13e991aa1c
commit
257a021669
|
|
@ -415,6 +415,25 @@ func handleMCPToolsList(w http.ResponseWriter, req mcpRequest) {
|
|||
"inputSchema": map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
||||
"annotations": readOnly,
|
||||
},
|
||||
{
|
||||
"name": "create_entry",
|
||||
"title": "Create Entry",
|
||||
"description": "Write a new health entry directly into a dossier. Use for saving AI-generated insights, supplement notes, nutrition logs, or any health observation. Requires write permission on the dossier. Category must be one of the values returned by list_categories (e.g. 'supplement', 'nutrition', 'tracker'). Type is a sub-type within the category (e.g. 'vitamin', 'meal', 'note'). Value is a short human-readable label. Summary is a one-line description. Data is optional structured JSON for additional fields.",
|
||||
"inputSchema": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"dossier": map[string]interface{}{"type": "string", "description": "Dossier ID (16-char hex)"},
|
||||
"category": map[string]interface{}{"type": "string", "description": "Category name (e.g. 'supplement', 'nutrition', 'tracker')"},
|
||||
"type": map[string]interface{}{"type": "string", "description": "Entry type within category (e.g. 'vitamin', 'meal', 'note')"},
|
||||
"value": map[string]interface{}{"type": "string", "description": "Short label or title for the entry"},
|
||||
"summary": map[string]interface{}{"type": "string", "description": "One-line description or AI-generated insight"},
|
||||
"data": map[string]interface{}{"type": "string", "description": "Optional JSON string with additional structured fields"},
|
||||
"parent": map[string]interface{}{"type": "string", "description": "Optional parent entry ID for hierarchical entries"},
|
||||
"timestamp": map[string]interface{}{"type": "number", "description": "Optional Unix timestamp (defaults to now)"},
|
||||
},
|
||||
"required": []string{"dossier", "category", "value"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sendMCPResult(w, req.ID, map[string]interface{}{"tools": tools})
|
||||
|
|
@ -530,6 +549,30 @@ func handleMCPToolsCall(w http.ResponseWriter, req mcpRequest, dossierID string)
|
|||
case "get_version":
|
||||
sendMCPResult(w, req.ID, mcpTextContent(fmt.Sprintf("Server: %s v%s", mcpServerName, mcpServerVersion)))
|
||||
|
||||
case "create_entry":
|
||||
dossier, _ := params.Arguments["dossier"].(string)
|
||||
category, _ := params.Arguments["category"].(string)
|
||||
typ, _ := params.Arguments["type"].(string)
|
||||
value, _ := params.Arguments["value"].(string)
|
||||
summary, _ := params.Arguments["summary"].(string)
|
||||
data, _ := params.Arguments["data"].(string)
|
||||
parent, _ := params.Arguments["parent"].(string)
|
||||
var ts int64
|
||||
if tsRaw, ok := params.Arguments["timestamp"]; ok {
|
||||
switch v := tsRaw.(type) {
|
||||
case float64:
|
||||
ts = int64(v)
|
||||
case int64:
|
||||
ts = v
|
||||
}
|
||||
}
|
||||
result, err := mcpCreateEntry(dossierID, dossier, category, typ, value, summary, data, parent, ts)
|
||||
if err != nil {
|
||||
sendMCPError(w, req.ID, -32000, err.Error())
|
||||
return
|
||||
}
|
||||
sendMCPResult(w, req.ID, mcpTextContent(result))
|
||||
|
||||
default:
|
||||
sendMCPError(w, req.ID, -32601, "Unknown tool: "+params.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inou/lib"
|
||||
)
|
||||
|
|
@ -372,3 +373,56 @@ func docToTranslation(e *lib.Entry, data map[string]interface{}) (string, error)
|
|||
}
|
||||
return result.Content[0].Text, nil
|
||||
}
|
||||
|
||||
// mcpCreateEntry writes a new entry to the specified dossier.
|
||||
// accessorID is the MCP session's dossier ID (the authenticated user).
|
||||
// dossier is the target dossier (may differ for shared family dossiers).
|
||||
func mcpCreateEntry(accessorID, dossier, category, typ, value, summary, data, parent string, ts int64) (string, error) {
|
||||
if dossier == "" {
|
||||
return "", fmt.Errorf("dossier required")
|
||||
}
|
||||
if value == "" {
|
||||
return "", fmt.Errorf("value required")
|
||||
}
|
||||
|
||||
// Resolve category int
|
||||
catInt, ok := lib.CategoryFromString[category]
|
||||
if !ok {
|
||||
// Build a helpful error listing valid categories
|
||||
var valid []string
|
||||
for k := range lib.CategoryFromString {
|
||||
valid = append(valid, k)
|
||||
}
|
||||
return "", fmt.Errorf("unknown category %q — valid: %s", category, strings.Join(valid, ", "))
|
||||
}
|
||||
|
||||
// Validate data JSON if provided
|
||||
if data != "" {
|
||||
var probe interface{}
|
||||
if err := json.Unmarshal([]byte(data), &probe); err != nil {
|
||||
return "", fmt.Errorf("data must be valid JSON: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if ts == 0 {
|
||||
ts = time.Now().Unix()
|
||||
}
|
||||
|
||||
e := &lib.Entry{
|
||||
DossierID: dossier,
|
||||
Category: catInt,
|
||||
Type: typ,
|
||||
Value: value,
|
||||
Summary: summary,
|
||||
Data: data,
|
||||
ParentID: parent,
|
||||
Timestamp: ts,
|
||||
}
|
||||
|
||||
if err := lib.EntryWrite(accessorID, e); err != nil {
|
||||
return "", fmt.Errorf("write failed: %v", err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Entry created: id=%s category=%s type=%s value=%q timestamp=%d",
|
||||
e.EntryID, category, typ, value, ts), nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue