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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Added

- `kagi mcp --tools` and `--exclude-tools` select which tools are exposed; `KAGI_MCP_TOOLS` provides an environment allowlist. Hidden tools cannot be called, and mutating tools still require explicit opt-in (#201).

- Static Linux release binaries for `x86_64-unknown-linux-musl` and `aarch64-unknown-linux-musl`, with archives, bare binaries, and SHA-256 checksums (#200).

## [0.21.1]
Expand Down
41 changes: 41 additions & 0 deletions docs/content/docs/commands/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ MCP specification's `initialize` handshake, `ping`, `tools/list`, and

```bash
kagi mcp [--default-output json|toon|pretty|compact|markdown|csv] [--enable-mutating-tools]
[--tools NAME,... | --exclude-tools NAME,...]
kagi mcp install [--target CLIENT]... [--all] [--dry-run]
kagi mcp setup [--target CLIENT]... [--all] [--dry-run]
kagi mcp auth [--target CLIENT]... [--all] [--dry-run]
Expand Down Expand Up @@ -156,6 +157,46 @@ Expose account and local-state mutation tools only when you want an agent to man
codex mcp add kagi-admin -- "$(command -v kagi)" mcp --enable-mutating-tools
```

## Choose which tools to expose

Expose only the tools your agent needs:

```bash
kagi mcp --tools kagi_search,kagi_batch_search,kagi_quick
codex mcp add kagi-search -- "$(command -v kagi)" mcp --tools kagi_search,kagi_quick
```

Or expose the enabled catalog except selected tools:

```bash
kagi mcp --exclude-tools kagi_fastgpt,kagi_extract
```

For clients where changing server arguments is awkward, set the comma-separated
`KAGI_MCP_TOOLS` environment variable in the server configuration:

```bash
KAGI_MCP_TOOLS=kagi_search,kagi_batch_search,kagi_quick kagi mcp
```

`--tools` and `--exclude-tools` cannot be combined. Either flag overrides
`KAGI_MCP_TOOLS`. Names are case-sensitive; surrounding whitespace is trimmed and
duplicates are ignored. Unknown or empty names are startup errors. Omitting both
flags and the environment variable preserves the default catalog.

Filters apply to both stable and draft `tools/list` and `tools/call`. Hidden tools
cannot be called directly; they return an unknown-tool JSON-RPC error. Excluding
all enabled tools produces an empty catalog. To include a mutating tool, also pass
`--enable-mutating-tools`; an allowlist alone does not enable mutations:

```bash
kagi mcp --enable-mutating-tools --tools kagi_lens_list,kagi_lens_create
```

Restart your MCP client after changing the filter so it refreshes its tool list.
The setup commands install plain `kagi mcp`; customize the saved arguments or
server environment to apply a filter.

## Tools

Default read/query tools:
Expand Down
18 changes: 18 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,24 @@ pub struct McpArgs {
#[arg(long, value_name = "FORMAT", value_enum)]
pub default_output: Option<OutputFormat>,

/// Expose only these comma-separated MCP tool names (or use KAGI_MCP_TOOLS)
#[arg(
long,
value_name = "NAME,...",
value_delimiter = ',',
conflicts_with = "exclude_tools"
)]
pub tools: Option<Vec<String>>,

/// Expose all enabled MCP tools except these comma-separated names
#[arg(
long,
value_name = "NAME,...",
value_delimiter = ',',
conflicts_with = "tools"
)]
pub exclude_tools: Option<Vec<String>>,

/// Expose MCP tools that mutate Kagi account or local CLI state
#[arg(long)]
pub enable_mutating_tools: bool,
Expand Down
72 changes: 71 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3254,14 +3254,84 @@ impl McpServerConfig {
}
}

fn with_tool_filter(
mut self,
tools: Option<&[String]>,
exclude_tools: &[String],
) -> Result<Self, KagiError> {
if tools.is_none() && exclude_tools.is_empty() {
return Ok(self);
}

let all_tools = build_mcp_tool_definitions(true);
let normalize_names = |names: &[String]| -> Result<BTreeSet<String>, KagiError> {
names
.iter()
.map(|name| {
let name = name.trim();
if name.is_empty() {
return Err(KagiError::Config(
"MCP tool filters must not contain empty names".into(),
));
}
if !all_tools.iter().any(|tool| tool["name"].as_str() == Some(name)) {
return Err(KagiError::Config(format!(
"Unknown MCP tool `{name}`. Run `kagi mcp` and call tools/list to inspect available tools"
)));
}
Ok(name.to_string())
})
.collect()
};
let included = tools.map(normalize_names).transpose()?;
let excluded = normalize_names(exclude_tools)?;
if let Some(included) = &included {
for name in included {
if !self
.tool_definitions
.iter()
.any(|tool| tool["name"].as_str() == Some(name.as_str()))
{
return Err(KagiError::Config(format!(
"MCP tool `{name}` requires --enable-mutating-tools"
)));
}
}
}
self.tool_definitions.retain(|tool| {
let name = tool["name"].as_str().expect("MCP tool has a name");
included.as_ref().is_none_or(|names| names.contains(name)) && !excluded.contains(name)
});
Ok(self)
}

fn default_output_or(&self, fallback: OutputFormat) -> OutputFormat {
self.default_output.clone().unwrap_or(fallback)
}
}

async fn run_mcp(args: McpArgs, profile: Option<&str>) -> Result<(), KagiError> {
let _json_lines = args.json_lines;
let config = McpServerConfig::new(args.default_output, args.enable_mutating_tools);
// Explicit CLI filters override the environment, including --exclude-tools.
let env_tools = if args.tools.is_none() && args.exclude_tools.is_none() {
match env::var("KAGI_MCP_TOOLS") {
Ok(value) => Some(value.split(',').map(str::to_string).collect::<Vec<_>>()),
Err(env::VarError::NotPresent) => None,
Err(env::VarError::NotUnicode(_)) => {
return Err(KagiError::Config(
"KAGI_MCP_TOOLS must be valid UTF-8".into(),
));
}
}
} else {
None
};
let tools = args.tools.or(env_tools);
let config = McpServerConfig::new(args.default_output, args.enable_mutating_tools)
.with_tool_filter(
tools.as_deref(),
args.exclude_tools.as_deref().unwrap_or_default(),
)?;
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line =
Expand Down
Loading
Loading