Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion internal/mcp/annotations_wire_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,15 @@ func TestAuthStatusAnnotationOverride(t *testing.T) {
require.True(t, entry.Destructive, "auth_status must declare destructiveHint=true (a sent email cannot be unsent)")
require.True(t, entry.OpenWorldHint, "auth_status classifies as open-world")

// A sibling read op without an override keeps the Safety mapping.
// A sibling read op without an override keeps the Safety mapping. Reads
// change no external state, so openWorldHint must stay false alongside
// readOnlyHint=true (directory validators reject readOnly+openWorld).
entry = catalogDescriptorToEntry(catalog.ToolDescriptor{
Name: "pins_list",
Safety: catalog.SafetyRead,
}, nil, nil)
require.True(t, entry.ReadOnly, "pins_list keeps SafetyRead -> readOnlyHint=true")
require.False(t, entry.OpenWorldHint, "pins_list is a read; openWorldHint must be false for read-only tools")
}

// requireSharedViewOrigin asserts every registered ui:// resource carries one
Expand Down
24 changes: 15 additions & 9 deletions internal/mcp/catalogsurface.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ func compiledHandler(cat catalog.Catalog, name string, resolveToken func(ctx con
// auth_status is the only one so far: it can trigger out-of-band sign-in
// communication (the SSO hand-off emails the human a verification link, which
// cannot be unsent), so the Claude/MCP directory validators classify it as
// non-read and destructive — a sent message is irreversible. Its hints must
// declare that contract rather than the local "reads config only" shape.
var readOnlyOverride = map[string]struct{ readOnly, destructive bool }{
"auth_status": {readOnly: false, destructive: true},
// non-read, destructive and open-world — a sent message is irreversible. Its
// hints must declare that contract rather than the local "reads config only"
// shape.
var readOnlyOverride = map[string]struct {
readOnly, destructive, openWorld bool
}{
"auth_status": {readOnly: false, destructive: true, openWorld: true},
}

// catalogDescriptorToEntry converts a compiler-produced catalog.ToolDescriptor
Expand All @@ -98,19 +101,22 @@ var readOnlyOverride = map[string]struct{ readOnly, destructive bool }{
// SafetyDestructive -> Destructive=true
// SafetyMutate -> neither
//
// Every compiled operation talks to the Pinner service (or the IPFS/Sia
// networks), so the open-world hint defaults to true; the hints may be
// corrected per tool via readOnlyOverride where the platform contract
// demands it (see auth_status).
// The open-world hint derives from Safety: mutating/destructive operations
// change publicly visible internet state (pins, websites, DNS), while reads
// change nothing external, so openWorldHint stays false for any SafetyRead
// operation. The hints may be corrected per tool via readOnlyOverride where
// the platform contract demands it (see auth_status).
//
// DirectVisible is left to markCurated (the curated product surface), matching
// how every other tool is promoted to tools/list.
func catalogDescriptorToEntry(d catalog.ToolDescriptor, cat catalog.Catalog, resolveToken func(ctx context.Context) (string, error)) *model.ToolEntry {
readOnly := d.Safety == catalog.SafetyRead
destructive := d.Safety == catalog.SafetyDestructive
openWorld := !readOnly
Comment thread
pcfreak30 marked this conversation as resolved.
if override, ok := readOnlyOverride[d.Name]; ok {
readOnly = override.readOnly
destructive = override.destructive
openWorld = override.openWorld
}
entry := model.ToolEntryFromDescriptor(model.ToolDescriptor{
Name: d.Name,
Expand All @@ -121,7 +127,7 @@ func catalogDescriptorToEntry(d catalog.ToolDescriptor, cat catalog.Catalog, res
OutputSchema: outputSchemaForCompiled(d.Safety, d.Interaction),
ReadOnly: readOnly,
Destructive: destructive,
OpenWorldHint: true,
OpenWorldHint: openWorld,
MCPTargets: toModelTargets(d.MCPTargets),
Handler: compiledHandler(cat, d.Name, resolveToken),
})
Expand Down
2 changes: 1 addition & 1 deletion internal/mcp/core/transfer/download_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewDownloadFileDescriptor(ipfsFn IPFSDownloadHandler, hd *Download, downloa
Title: "Download IPFS content to a file",
Description: downloadFileDescription(hd != nil, tunnelOpenAI),
Category: model.CategoryCore,
OpenWorldHint: true, // fetches content from the IPFS network
OpenWorldHint: false, // closed workflow: pulls content into local storage/filedrop; publishes nothing to the public internet
Comment thread
pcfreak30 marked this conversation as resolved.
// The input schema advertises only the sink values valid for the running
// transport (drop only when a reachable HTTP mux exists on a non-OpenAI
// tunnel), matching capabilities().download_sink_modes so the published
Expand Down
2 changes: 1 addition & 1 deletion internal/mcp/vault/vault_get_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func NewVaultGetFileDescriptor(getFn transfer.VaultGetHandler, hd *transfer.Down
Title: "Download a file from the Pinner vault",
Description: vaultGetFileDescription(hd != nil, tunnelOpenAI),
Category: model.CategoryCore,
OpenWorldHint: true, // may fetch file bytes from the Sia network
OpenWorldHint: false, // closed workflow: pulls the caller's vault bytes to a local sink/filedrop; publishes nothing
Comment thread
pcfreak30 marked this conversation as resolved.
// The input schema advertises only the sink values valid for the running
// server (drop only when a reachable HTTP mux exists on a non-OpenAI
// tunnel), matching capabilities().download_sink_modes.
Expand Down
Loading