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
27 changes: 19 additions & 8 deletions docs/docs/reference/mcp-tools.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: MCP tool reference
description: Exact inputs, successful structured outputs, discovery behavior, and errors for the three CodeMode MCP tools.
description: Exact inputs, listed descriptions, successful structured outputs, discovery behavior, and errors for the three CodeMode MCP tools.
---

# MCP tool reference
Expand All @@ -13,11 +13,11 @@ description: Exact inputs, successful structured outputs, discovery behavior, an

Each input is an object with one required string property. Additional properties are rejected by the SDK before subject resolution or service work. Every valid call then resolves a trusted subject through `mcpserver.InvocationResolver` before it reaches the CodeMode service.

On success, the official SDK returns the documented value in `CallToolResult.StructuredContent` and one JSON `TextContent` item that mirrors it. The schemas below describe the structured value, not the surrounding MCP result.
On success, the official SDK returns the documented value in `CallToolResult.StructuredContent` and one JSON `TextContent` item that mirrors it. The schemas below describe the structured value, not the surrounding MCP result. Each tool's listed `tools/list` description is the model-facing authoring contract for that tool.

## `search_api`

Search the names and summaries of enabled capabilities.
Search enabled names and summaries with a short literal substring. Retry an empty result with a shorter term.

### Input

Expand All @@ -34,7 +34,7 @@ Search the names and summaries of enabled capabilities.
}
```

The raw query is limited by `MaxSearchQueryBytes` before trimming or case normalization. Whitespace padding counts. CodeMode then trims surrounding whitespace and normalizes case. Matching is a substring search over capability names and summaries. A blank normalized query returns an empty array.
The raw query is limited by `MaxSearchQueryBytes` before trimming or case normalization. Whitespace padding counts. CodeMode then trims surrounding whitespace and normalizes case. Matching is a short literal substring over capability names and summaries. A blank normalized query or any other empty result is `[]`, not `null`. Retry an empty result with a shorter term. Search does not add fuzzy matching, aliases, or extra query rewriting.

Results are sorted by exact dotted name and limited by `MaxSearchResults`. Static filtering happens before search, so disabled capabilities never appear.

Expand Down Expand Up @@ -66,7 +66,7 @@ The structured content itself is the array described above, not an object that w

## `describe_api`

Describe one enabled capability by the exact dotted `name` returned by `search_api`.
Describe one enabled capability by the exact name returned by search_api, without whitespace or case changes.

### Input

Expand All @@ -83,7 +83,7 @@ Describe one enabled capability by the exact dotted `name` returned by `search_a
}
```

Name lookup is exact. It neither trims nor case-folds, and it does not perform search, prefix expansion, or fuzzy matching. Clients must pass the exact `name` returned by `search_api`. An unknown or disabled name returns `capability not found`.
Name lookup is exact. It neither trims nor case-folds, and it does not perform search, prefix expansion, or fuzzy matching. Pass the exact `name` returned by `search_api`, without whitespace or case changes. An unknown or disabled name returns `capability not found`.

For the site-wide sample, the requested name is `records.lookup`. Its stable ID, `records.entry.lookup`, is intentionally not part of this tool input or output.

Expand Down Expand Up @@ -145,7 +145,7 @@ The sample capability therefore describes input fields `key` (`str`, required) a

## `execute`

Execute one bounded Starlark program against the enabled capability namespace.
Execute one Starlark program that defines def main(): with zero arguments, calls only names confirmed through search_api and describe_api inside main, and returns main's final result.

### Input

Expand All @@ -162,7 +162,7 @@ Execute one bounded Starlark program against the enabled capability namespace.
}
```

The source must define `main` as a function with no parameters. Source loading cannot call native capabilities; calls are accepted only while `main` runs. Module loading is disabled.
The source must define `def main():` as a function with zero arguments. Source loading cannot call native capabilities; calls are accepted only while `main` runs, and only for names confirmed through `search_api` and `describe_api`. Module loading is disabled.

Capabilities are available by dotted name. The sample native call is `records.lookup(key="alpha", limit=2)`. Native calls accept keyword arguments only. Duplicate keyword syntax is rejected by the Starlark parser as `invalid program` before authorization or handler dispatch. Positional, unknown, missing, incorrectly typed, and out-of-range arguments reach binding and map to `invalid capability arguments`. For the sample, `key` is required and `limit` can be omitted, `None`, or an integer in the signed 64-bit range.

Expand Down Expand Up @@ -195,6 +195,17 @@ The `result` property is the final converted return value from `main`. Its runti

Only the final converted value crosses the execution boundary. `print` output is discarded. Globals, source-loading values, intermediate expressions, and native results that are not included in the final return value are not added to structured output. The successful envelope contains only `result`.

## Authoring and recovery

The listed descriptions above are the model-facing contract. Recovery uses the same fixed coarse errors on this page. Error payloads stay non-disclosing; they do not gain diagnostic detail, aliases, or suggested alternate names.

- Search with a short literal substring over enabled names and summaries. If the result is empty, retry with a shorter term.
- Pass `describe_api` the exact `name` returned by `search_api`, without whitespace or case changes.
- Compare native call arguments with the `describe_api` field shapes before `execute`.
- Define `def main():` with zero arguments. Call only names confirmed through `search_api` and `describe_api` inside `main`. Return `main`'s final result.
- After `resource limit exceeded`, reduce program or result complexity and retry.
- `permission denied` and `authorization policy failure` are outcomes for that call only. They do not disclose policy rules.

## Errors

After a well-formed call reaches the adapter, a resolver or service failure becomes a successful MCP protocol response with `isError` set and one coarse text item. The adapter does not expose wrapped policy diagnostics, handler errors, panic values, stack details, credentials, source, or arguments.
Expand Down
6 changes: 3 additions & 3 deletions mcpserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ func New(service Service, resolver InvocationResolver) (*mcp.Server, error) {
server := mcp.NewServer(&mcp.Implementation{Name: "codemode", Version: "1"}, nil)
mcp.AddTool(server, &mcp.Tool{
Name: "search_api",
Description: "Search enabled capabilities by a bounded query string.",
Description: "Search enabled names and summaries with a short literal substring. Retry an empty result with a shorter term.",
}, bound.search)
mcp.AddTool(server, &mcp.Tool{
Name: "describe_api",
Description: "Describe one enabled capability by its exact name.",
Description: "Describe one enabled capability by the exact name returned by search_api, without whitespace or case changes.",
}, bound.describe)
mcp.AddTool(server, &mcp.Tool{
Name: "execute",
Description: "Execute one bounded Starlark program and return only its final result.",
Description: "Execute one Starlark program that defines def main(): with zero arguments, calls only names confirmed through search_api and describe_api inside main, and returns main's final result.",
}, bound.execute)
return server, nil
}
Expand Down
51 changes: 50 additions & 1 deletion mcpserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,50 @@ func TestNewRejectsMissingDependencies(t *testing.T) {
}
}

// TestNewRegistersExactlyThreeTools proves the adapter exposes only the three official tools.
// TestNewRegistersExactlyThreeTools proves the adapter exposes only the three official tools
// and lists authoring guidance on each description.
func TestNewRegistersExactlyThreeTools(t *testing.T) {
session := newTestSession(t, mocks.NewMockService(t), mocks.NewMockInvocationResolver(t))

listed, err := session.client.ListTools(t.Context(), nil)
require.NoError(t, err)
require.Len(t, listed.Tools, 3)
assert.Equal(t, []string{"describe_api", "execute", "search_api"}, toolNames(listed.Tools))

tests := []struct {
// name is the official listed tool.
name string

// cues are required authoring phrases on the listed description.
cues []string
}{
{
name: "search_api",
cues: []string{"short literal substring", "shorter term"},
},
{
name: "describe_api",
cues: []string{"exact name returned by search_api", "without whitespace or case changes"},
},
{
name: "execute",
cues: []string{
"def main():",
"zero arguments",
"inside main",
"confirmed through search_api and describe_api",
"final result",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
description := listedDescription(t, listed.Tools, tt.name)
for _, cue := range tt.cues {
assert.Contains(t, description, cue)
}
})
}
}

// TestSDKRejectsMalformedArgumentsBeforeResolution proves schema validation owns malformed tool input.
Expand Down Expand Up @@ -524,6 +560,19 @@ func toolNames(tools []*mcp.Tool) []string {
return names
}

// listedDescription returns the tools/list description for name.
func listedDescription(t *testing.T, tools []*mcp.Tool, name string) string {
t.Helper()

for _, tool := range tools {
if tool.Name == name {
return tool.Description
}
}
require.FailNow(t, "expected listed tool "+name)
return ""
}

// requireToolValidationError asserts the SDK rejected malformed typed arguments.
func requireToolValidationError(t *testing.T, result *mcp.CallToolResult) {
t.Helper()
Expand Down