Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ If wigolo earns a place in your setup, three things keep it going: a ⭐ **star*
- **Browser won't launch on Linux** — `wigolo warmup --browser` installs the OS libraries (or prints the exact command).
- **Native build error / unusual Node** — use an LTS: **Node 20, 22, or 24**.
- **Behind a proxy** — `USE_PROXY=true` + `PROXY_URL`; add `NODE_EXTRA_CA_CERTS` for TLS-inspecting proxies.
- **Your agent asks permission on every call** — allow the tools in your client, then restart it; rules are read at session start. [Details](docs/troubleshooting.md#your-agent-keeps-asking-permission).

The full guide covers per-symptom fixes, a "what still works when X fails" map, platform notes (incl. linux-arm64), and offline installs: **[docs/troubleshooting.md](docs/troubleshooting.md)**.

Expand Down
2 changes: 2 additions & 0 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ wigolo carries registry manifests at the repo root — `smithery.yaml`, `glama.j
npx wigolo init --agents=claude-code,cursor
```

If your agent prompts for permission on every wigolo tool call after wiring, see [troubleshooting](./troubleshooting.md#your-agent-keeps-asking-permission).

For OpenCode, wigolo writes the global `~/.config/opencode/opencode.json` entry in OpenCode's local MCP format:

```json
Expand Down
38 changes: 38 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ wigolo doctor --fix # repairs the known failure classes automatically
| `wigolo serve` exits: port in use | The daemon deliberately does not auto-rebind. The error names a free port to retry with, e.g. `wigolo serve --port 3334`. |
| `wigolo serve` refuses to start on a non-loopback host | Working as designed (fail-closed). Set `WIGOLO_API_TOKEN` / `WIGOLO_API_TOKEN_FILE`, or explicitly pass `--allow-unauthenticated`. See [self-hosting](./self-hosting.md#binding-beyond-loopback). |
| Fetch result says `blocked_by_challenge` | See [below](#blocked_by_challenge). |
| Your agent asks permission before every wigolo tool call | See [below](#your-agent-keeps-asking-permission). |
| Search results feel thin / an engine seems dead | Degraded engines are *reported*, not hidden — check `engine_warnings`, `engine_telemetry`, and `engine_pool` in the response, and `wigolo doctor`'s per-engine table (it names the env var when an engine just wants a key, e.g. `WIGOLO_GITHUB_TOKEN`, `BRAVE_API_KEY`). |
| Results are stale | Pass `force_refresh: true` (news, prices, changelogs), or clear scoped entries: `wigolo cache clear --url-pattern="*example.com*"`. Lifetimes are tunable: `CACHE_TTL_SEARCH`, `CACHE_TTL_CONTENT`. |
| Everything fails behind a corporate proxy | Set `USE_PROXY=true` and `PROXY_URL` (credentials go to the OS keychain, not disk). See [configuration](./configuration.md#fetch-and-browser-engine). |
Expand Down Expand Up @@ -50,6 +51,43 @@ Two honest facts to calibrate expectations:
- **IP reputation is scored.** From datacenter IPs (VPS, CI, cloud), some challenge-protected sites will not clear even though the identical request works from a residential connection. That's a property of where you're running, not a knob wigolo forgot.
- **The opt-in lever is a proxy** whose IP reputation matches your legitimate-research use — see [self-hosting](./self-hosting.md#the-datacenter-ip-reality). Credentials are keychain-stored, and politeness (robots.txt, per-domain rate limits) still applies.

## Your agent keeps asking permission

Every wigolo tool reports MCP capability hints (`readOnlyHint`, `destructiveHint`,
`idempotentHint`, `openWorldHint`) in its `tools/list` entry, and most clients use those to
auto-approve the read-only ones. Eight of the ten are read-only. `cache` and `watch` are not,
because `cache` accepts `clear` and `watch` creates and deletes jobs — clients are told so
deliberately, and prompting on those two is correct behavior.

Clients that ignore the hints need an explicit allow rule.

**Claude Code in plan mode** (observed on 2.1.220). Plan mode refuses any MCP tool that is not
annotated read-only, and it decides that *before* it looks at your allow rules — so an allow
rule cannot lift it.
Before wigolo shipped these hints, every tool was treated as non-read-only and prompted on every
call in plan mode no matter what was in `settings.json`. If you are on an older wigolo, upgrade.
`cache` and `watch` still prompt in plan mode, correctly: they change state.

**Claude Code, normal modes.** Add to `~/.claude/settings.json`:

```json
{
"permissions": {
"allow": ["mcp__wigolo__*"]
}
}
```

Then **restart Claude Code**. This is the step people miss: permission rules are read once at
session start, so a session that was already open when you edited the file keeps prompting until
you restart it, and it looks like the rule did not work.

The `mcp__wigolo__` prefix must be literal — the server segment cannot contain a glob, so
`mcp__*` is skipped with a warning and approves nothing.

If the server name is not `wigolo` in your config, use whatever name you registered it under —
the rule matches the configured server name, not the package name. `claude mcp list` shows it.

## Platform notes

**Node version.** wigolo runs on **Node 20, 22, or 24** (LTS). Very new or unusual Node builds may not have prebuilt native binaries yet and will try to compile from source (which needs a C/C++ toolchain) — stick to an LTS to avoid that.
Expand Down
85 changes: 85 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,57 +317,142 @@ export function createMcpServer(subsystems: Subsystems): Server {
};
});

// Capability hints per MCP `tools/list`. Hosts use these to decide whether a
// call needs a permission prompt, so each one describes the WIDEST behaviour
// its tool can reach, not the common case.
//
// Read-only tools still populate the local content cache. That store is an
// implementation detail rather than caller-visible state, which is why they
// stay `readOnlyHint: true` while `cache` — the tool that exposes the store
// directly — does not.
//
// `idempotentHint` is inert wherever `readOnlyHint` is true (spec: "meaningful
// only when readOnlyHint == false"), so those tools carry `true` for
// consistency rather than as a claim about output stability.
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'fetch',
description: TOOL_DESCRIPTIONS.fetch,
inputSchema: FETCH_TOOL_SCHEMA,
annotations: {
title: 'Fetch a page',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
{
name: 'search',
description: TOOL_DESCRIPTIONS.search,
inputSchema: SEARCH_TOOL_SCHEMA,
annotations: {
title: 'Web search',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'crawl',
description: TOOL_DESCRIPTIONS.crawl,
inputSchema: CRAWL_TOOL_SCHEMA,
annotations: {
title: 'Crawl a site',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'cache',
description: TOOL_DESCRIPTIONS.cache,
inputSchema: CACHE_TOOL_SCHEMA,
// `clear` deletes rows; `check_changes` re-fetches every matching URL
// over the network, so this is not a closed-world tool either.
annotations: {
title: 'Search or clear the local cache',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
},
},
{
name: 'extract',
description: TOOL_DESCRIPTIONS.extract,
inputSchema: EXTRACT_TOOL_SCHEMA,
annotations: {
title: 'Extract structured data',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'find_similar',
description: TOOL_DESCRIPTIONS.find_similar,
inputSchema: FIND_SIMILAR_TOOL_SCHEMA,
annotations: {
title: 'Find similar pages',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'research',
description: TOOL_DESCRIPTIONS.research,
inputSchema: RESEARCH_TOOL_SCHEMA,
annotations: {
title: 'Deep research',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'agent',
description: TOOL_DESCRIPTIONS.agent,
inputSchema: AGENT_TOOL_SCHEMA,
annotations: {
title: 'Autonomous data gathering',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
},
{
name: 'diff',
description: TOOL_DESCRIPTIONS.diff,
inputSchema: DIFF_TOOL_SCHEMA,
annotations: {
title: 'Diff two versions',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
},
{
name: 'watch',
description: TOOL_DESCRIPTIONS.watch,
inputSchema: WATCH_TOOL_SCHEMA,
// `create`/`delete`/`pause`/`resume` mutate the persistent job store.
annotations: {
title: 'Watch a URL for changes',
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
},
},
],
}));
Expand Down
Loading