Skip to content
Merged
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ go.work.sum

# Editor/IDE
.idea/
*.iml
.vscode/*
!.vscode/settings.json

# This repo's own tapper project config is developer-local: it records the
# maintainer's default keg and flight, which are not shared state.
.tapper/

# macOS
.DS_Store

Expand Down
2 changes: 1 addition & 1 deletion docs/ai-coding-agents/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ hidden controls. To change a config-driven session, run `tap use --flight
existing thread.

If no flight is selected, MCP still connects in recovery-only mode. Codex can
use `list_flights`, `flight_show`, `auth_status`, and `config`, while KEG tools
use `orient`, `list_flights`, `flight_show`, and credential-safe `auth_info`, while KEG tools
remain locked. Ask the user to run `tap use --flight @namespace/+slug`, then
call `mcp__tapper__orient` to restore the normal tool surface. If the MCP tools
are unavailable, report the unavailable connection, ask the user to reconnect
Expand Down
80 changes: 55 additions & 25 deletions docs/ai-coding-agents/mcp-setup.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# MCP Server Setup

The `tap mcp` command starts a Model Context Protocol server on stdio, exposing
KEG operations as tools. The local full surface publishes flight authority at
initialization and on explicit orientation. Without one, the server starts in a
recovery-only state so the host can inspect flights safely. This page is the
advanced manual path for MCP hosts that are not using the bundled Claude Code
or Codex integrations.
the same agent-safe tools and resources as Tapper Hub's authenticated `/mcp`
endpoint — the one difference being attachment transfers, where `tap mcp` can
also read and write local paths because it runs on your machine. Both publish
immutable flight authority at initialization and on explicit orientation.
Without one, the server starts in a recovery-only state so the host can inspect
flights safely. This page is the advanced manual path for MCP hosts that are not
using the bundled Claude Code or Codex integrations.

Most users should use the official one-command installs in the project README's
[Connect AI Agents](../../README.md#connect-ai-agents) section. `tap integrate
Expand Down Expand Up @@ -72,12 +74,12 @@ With a default keg:

## Available Tools

The MCP server exposes the same operating surface as the CLI. Exact tool
availability follows the installed Tapper version; inspect your MCP host's tool
list for the live surface.
The MCP server exposes a shared agent surface rather than every machine-local
CLI capability. Exact tool availability follows the installed Tapper version
and the active flight; inspect your MCP host's tool list for the live surface.

When no flight is selected, the visible list is intentionally restricted to
`orient`, `list_flights`, `flight_show`, `auth_status`, and `config`. `orient`
`orient`, `list_flights`, `flight_show`, and `auth_info`. `orient`
and any guessed KEG-tool call explain that KEG tools are locked and direct the
agent to inspect flights through MCP, ask the user to run `tap use --flight
@namespace/+slug`, and call `orient` again on the same connection.
Expand Down Expand Up @@ -112,7 +114,7 @@ agent to inspect flights through MCP, ask the user to run `tap use --flight
| Tool | Description |
| --- | --- |
| `index`, `list_indexes`, `index_cat` | Rebuild or inspect indexes |
| `doctor` | Check keg health |
| `doctor` | Check only the selected keg's health (not local Tapper configuration) |
| `node_history`, `node_snapshot`, `node_snapshot_view`, `node_restore` | Manage node snapshots |
| `lock_acquire`, `lock_release`, `lock_status`, `lock_force_release` | Coordinate cross-process node locks |

Expand All @@ -123,31 +125,52 @@ agent to inspect flights through MCP, ask the user to run `tap use --flight
| `list_files`, `upload_file`, `download_file`, `delete_file` | Manage file attachments |
| `list_images`, `upload_image`, `download_image`, `delete_image` | Manage image attachments |

### Organization And Keg Administration
The transfer tools come in two variants, chosen by whether the server shares a
filesystem with the agent driving it.

Local `tap mcp` runs on your machine, so a path names the same file on both
sides. It publishes the full round-trip: `upload_file` and `upload_image` accept
`source_path` and `file:` URIs alongside `data_base64`, data URIs, and embedded
resources; `download_file` writes to `dest_path`; and `download_image` takes an
optional `dest_path`, returning the image as MCP content when you omit it.

Hosted `/mcp` shares no filesystem with the agent, so a path there would name
the server's own disk. Its uploads accept only embedded resources, data URIs,
and base64 bytes; `download_image` always returns MCP image content; and
`download_file` is not registered. Those fields are absent from the published
schema rather than refused at call time, so a hosted agent never has the
vocabulary to ask.

### Discovery And Identity

| Tool | Description |
| --- | --- |
| `keg_list` | List visible kegs on a hub |
| `keg_visibility` | Set keg visibility |
| `namespace_list` | Inspect namespaces |
| `auth_status` | Inspect authentication state |
| `keg_list` | List identity-authorized kegs filtered through the active flight |
| `auth_info` | Return structured credential-safe `identities[]` and flight-filtered `kegs[]` |

User and role management tools are intentionally not exposed over MCP for now;
manage namespace members and keg grants through the hub UI.
Each identity includes only its hub locator, user ID, username, display name,
default namespace, and namespace names. Tokens, email, scopes, cookies, expiry,
and session data are never returned. Local MCP reports every configured
authenticated Hub identity; hosted MCP reports its single authenticated user.

### Automation And Setup

| Tool | Description |
| --- | --- |
| `repo_init` | Initialize a keg destination |
| `config`, `config_template` | Read config or starter templates |
| `import_from_keg` | Import nodes from another keg |
| `export`, `import` | Export or import keg archives |
| `graph` | Render a keg graph |
| `orient` | Return the shared KEG system orientation payload |
| `list_flights`, `flight_show` | Discover and inspect visible flights |
| `flight_create`, `flight_edit`, `flight_delete` | Manage other flights when the active flight grants `manage_flights` and the identity owns/administers the target namespace |
| `license` | Read bundled license text |
| `flight_create`, `flight_edit`, `flight_delete` | Manage Hub-backed flights when the active flight grants `manage_flights` and the identity owns/administers the target namespace |

MCP does not expose Tapper configuration, config templates, repository setup,
archive import/export, raw auth status, license text, keg visibility, or
namespace administration. Those remain external CLI, configuration, or Hub UI
operations.

`import_from_keg` requires editor identity and flight authority on the source
when `leave_stubs` is requested, because that option rewrites source nodes.
Both transports also publish `tapper://orient` and the
`tapper://node/{node_id}{?keg}` resource template.

## Keg Targeting

Expand All @@ -167,6 +190,11 @@ server-owned session state. Config-driven servers adopt configuration changes
only through explicit orientation; `tap mcp --flight` stays bound to that
identity for its process lifetime.

Hosted `/mcp` instead selects the authenticated account's global MCP flight
preference. A successful self-edit adopts the exact returned flight immediately;
a self-delete enters recovery immediately. Mutation tools disappear as soon as
the adopted flight no longer grants `manage_flights`.

## Troubleshooting

### Server Not Responding
Expand All @@ -189,8 +217,10 @@ tap use --flight @acme/+release-42
tap mcp --flight @acme/+release-42
```

After `tap use`, call `orient` on the existing session. A failed refresh keeps
the last valid authority; a blank selection intentionally enters recovery mode.
After `tap use`, call `orient` on the existing session. A failed ordinary
refresh keeps the last valid authority; a blank selection intentionally enters
recovery mode. If local configuration still names a flight deleted through MCP,
`orient` reports the stale external reference until configuration is changed.

### Logs

Expand Down
33 changes: 26 additions & 7 deletions docs/configuration/flights.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Flights

A **flight** is the required authorization and instruction context for a local
full MCP session. Flight manifests live separately from Tapper configuration.
A **flight** is the required authorization and instruction context for an MCP
session. Flight manifests live separately from Tapper configuration.
`tap bootstrap` can persist a machine-wide baseline in the user config, while a
project can persist a more specific selection in `.tapper/config.yaml`. The
server resolves fresh orientation during MCP initialization and again whenever
Expand Down Expand Up @@ -104,8 +104,17 @@ modeline are ignored when deciding whether the manifest changed.
MCP always exposes `list_flights` and `flight_show`. It exposes
`flight_create`, `flight_edit`, and `flight_delete` only while the session's
active flight grants `manage_flights`; direct calls are checked server-side as
well. A session can never edit or delete its own active flight. `flight_edit`
is a partial update where omitted fields retain their current values.
well. `flight_edit` is a partial update where omitted fields retain their
current values. A Hub-backed active flight may edit or delete itself:

- a successful self-edit immediately adopts the exact returned manifest,
cover, instructions, and capabilities before the response is released;
- removing `manage_flights` therefore removes the mutation tools immediately;
- a successful self-delete immediately enters recovery-only mode;
- editing or deleting another flight does not change current session authority.

Local `flights.d` manifests remain MCP read-only. Flight mutations always use
normal Hub authorization in addition to the active flight capability.

## Behavior

Expand All @@ -118,9 +127,9 @@ is a partial update where omitted fields retain their current values.
- `full_access` permits admin-class flight operations outside the cover, but
does not bypass normal identity authorization or implicitly grant
`manage_flights`.
- Without a selected flight, the local MCP server starts in recovery-only mode
and lists only `orient`, `list_flights`, `flight_show`, `auth_status`, and
`config`. After selecting a flight, call `orient` on the same connection.
- Without a selected flight, MCP starts in recovery-only mode and lists only
`orient`, `list_flights`, `flight_show`, and credential-safe `auth_info`.
After selecting a flight outside MCP, call `orient` on the same connection.
- Config-driven `tap mcp` reloads user, project, and environment configuration
on every orientation. A successful orientation atomically replaces session
authority; configuration changes alone do nothing.
Expand All @@ -129,6 +138,16 @@ is a partial update where omitted fields retain their current values.
manifest, cover, and instructions.
- A failed refresh preserves the last valid authority. An intentionally blank
config selection clears authority and enters recovery mode.
- If a self-edit is persisted but exact orientation rendering fails, the tool
reports that the update was applied and enters recovery instead of retaining
stale authority.
- Hosted `/mcp` selects the account-wide MCP flight preference. Local
initialization and `orient` select explicit `--flight`, then `TAP_FLIGHT`,
the nearest project config, and finally the user baseline.
- Hosted self-deletion clears the account preference through the flight foreign
key. A local config that still names a deleted flight remains a stale external
reference: later `orient` reports it and the session stays in recovery until
configuration is changed outside MCP.
- In-flight calls finish under the context captured when they began. Calls that
start after orientation use the newly published context.
- Direct CLI commands such as `tap cat`, `tap edit`, and `tap create` ignore
Expand Down
1 change: 0 additions & 1 deletion integrations/content/tool-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
| `mcp__tapper__cat` | Read one or more node bodies. Supports `meta_only`, `content_only`, `stats_only`, and `tag` expression selection as an alternative to explicit node IDs. |
| `mcp__tapper__links` | Outbound links from a node. |
| `mcp__tapper__backlinks` | Inbound links to a node. |
| `mcp__tapper__graph` | Graph traversal for multi-hop relationships. |
| `mcp__tapper__list_indexes`, `mcp__tapper__index_cat` | Read generated index files (tag index, changelog, and others). |
| `mcp__tapper__keg_settings` | Read targeted title, summary, updated metadata, and instructions for one or more selected KEGs; batches accept up to 100 canonical references. |

Expand Down
1 change: 0 additions & 1 deletion integrations/rendered/claude/tapper/skills/tapper/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ no KEGs.
| `mcp__tapper__cat` | Read one or more node bodies. Supports `meta_only`, `content_only`, `stats_only`, and `tag` expression selection as an alternative to explicit node IDs. |
| `mcp__tapper__links` | Outbound links from a node. |
| `mcp__tapper__backlinks` | Inbound links to a node. |
| `mcp__tapper__graph` | Graph traversal for multi-hop relationships. |
| `mcp__tapper__list_indexes`, `mcp__tapper__index_cat` | Read generated index files (tag index, changelog, and others). |
| `mcp__tapper__keg_settings` | Read targeted title, summary, updated metadata, and instructions for one or more selected KEGs; batches accept up to 100 canonical references. |

Expand Down
1 change: 0 additions & 1 deletion integrations/rendered/codex/tapper/skills/tapper/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ no KEGs.
| `mcp__tapper__cat` | Read one or more node bodies. Supports `meta_only`, `content_only`, `stats_only`, and `tag` expression selection as an alternative to explicit node IDs. |
| `mcp__tapper__links` | Outbound links from a node. |
| `mcp__tapper__backlinks` | Inbound links to a node. |
| `mcp__tapper__graph` | Graph traversal for multi-hop relationships. |
| `mcp__tapper__list_indexes`, `mcp__tapper__index_cat` | Read generated index files (tag index, changelog, and others). |
| `mcp__tapper__keg_settings` | Read targeted title, summary, updated metadata, and instructions for one or more selected KEGs; batches accept up to 100 canonical references. |

Expand Down
4 changes: 2 additions & 2 deletions pkg/cli/cmd_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func runAuthLogin(ctx context.Context, deps *Deps, p authLoginParams) (*authLogi
// unflagged login lands on the configured default — or the compiled-in
// DefaultHubURL — without forcing every invocation to repeat --hub. An
// explicit/selected URL still wins.
cfg, err := deps.Tap.ConfigService.Config(true)
cfg, err := deps.Tap.ConfigService.Config()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -320,7 +320,7 @@ platform). For scripts, pass --hub and pipe a token to --with-token:
// chain / compiled-in default.
selectedHub := hubURL
if selectedHub == "" && isTTY && !withToken {
cfg, err := deps.Tap.ConfigService.Config(true)
cfg, err := deps.Tap.ConfigService.Config()
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/cli/cmd_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ func bootstrapUserFlight(deps *Deps) string {
if deps == nil || deps.Tap == nil {
return ""
}
cfg, err := deps.Tap.ConfigService.UserConfig(false)
cfg, err := deps.Tap.ConfigService.ReadUserConfigFile()
if err != nil || cfg == nil {
return ""
}
Expand Down
59 changes: 59 additions & 0 deletions pkg/cli/cmd_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,65 @@ func TestConfigCommand_ExplainFlagWithEnvVar(t *testing.T) {
require.Contains(t, stdout, "source: env vars")
}

func TestConfigCommand_ProjectFlightPrecedenceMatchesOrient(t *testing.T) {
t.Parallel()

sb := NewSandbox(t)
project := "/home/testuser/work/project"
descendant := project + "/src/pkg"
require.NoError(t, sb.Setwd(descendant))
require.NoError(t, sb.Runtime().AtomicWriteFile(
"/home/testuser/.config/tapper/config.yaml",
[]byte(`flight: +baseline
fallbackNamespace: local
hubs:
home:
kind: local
basePath: /home/testuser/kegs
`), 0o644))
require.NoError(t, sb.Runtime().AtomicWriteFile(
project+"/.tapper/config.yaml", []byte("flight: +project\n"), 0o644))

for slug, instructions := range map[string]string{
"baseline": "Baseline instructions",
"project": "Project instructions",
"environment": "Environment instructions",
"explicit": "Explicit instructions",
} {
require.NoError(t, sb.Runtime().AtomicWriteFile(
"/home/testuser/kegs/flights.d/"+slug+".yaml",
[]byte("title: "+slug+"\ninstructions: "+instructions+"\n"), 0o644))
}

explained := NewProcess(t, false, "config", "--explain", "flight").Run(sb.Context(), sb.Runtime())
require.NoError(t, explained.Err)
require.Contains(t, string(explained.Stdout), "flight = +project")
require.Contains(t, string(explained.Stdout), "source: project config")

oriented := NewProcess(t, false, "orient").Run(sb.Context(), sb.Runtime())
require.NoError(t, oriented.Err)
require.Contains(t, string(oriented.Stdout), "+project")
require.Contains(t, string(oriented.Stdout), "Project instructions")
require.NotContains(t, string(oriented.Stdout), "Baseline instructions")

require.NoError(t, sb.Runtime().Env().Set("TAP_FLIGHT", "+environment"))
explained = NewProcess(t, false, "config", "--explain", "flight").Run(sb.Context(), sb.Runtime())
require.NoError(t, explained.Err)
require.Contains(t, string(explained.Stdout), "flight = +environment")
require.Contains(t, string(explained.Stdout), "source: env vars")

oriented = NewProcess(t, false, "orient").Run(sb.Context(), sb.Runtime())
require.NoError(t, oriented.Err)
require.Contains(t, string(oriented.Stdout), "+environment")
require.Contains(t, string(oriented.Stdout), "Environment instructions")

oriented = NewProcess(t, false, "--flight", "+explicit", "orient").Run(sb.Context(), sb.Runtime())
require.NoError(t, oriented.Err)
require.Contains(t, string(oriented.Stdout), "+explicit")
require.Contains(t, string(oriented.Stdout), "Explicit instructions")
require.NotContains(t, string(oriented.Stdout), "Environment instructions")
}

func TestConfigCommand_ShowSourcesFlag(t *testing.T) {
t.Parallel()

Expand Down
Loading
Loading