Skip to content

[AI-1134] kcap mcp memory — team memory MCP server + persisted machine id#239

Open
alexeyzimarev wants to merge 3 commits into
mainfrom
ai-1134-memory-mcp
Open

[AI-1134] kcap mcp memory — team memory MCP server + persisted machine id#239
alexeyzimarev wants to merge 3 commits into
mainfrom
ai-1134-memory-mcp

Conversation

@alexeyzimarev

Copy link
Copy Markdown
Member

Adds the CLI side of Agent Memories (AI-1134) — server-side shared memories for coding agents, scoped user/team/org with repo + machine context.

What's in here

  • kcap mcp memory — stdio MCP server (copy of the McpSessionsServer skeleton) with six tools: search_memories, get_memory, save_memory, update_memory, rescope_memory, archive_memory. Tool descriptions steer agents to search before saving and prefer update_memory on a reported near-duplicate.
  • Request bodies are snake_case (repo_hash, machine_tag, …) matching the server's global SnakeCaseLower JSON policy; responses pass through as raw text. Bodies built with JsonObject — no McpJsonContext additions, AOT publish clean (no new IL3050/IL2026).
  • Persisted machine idmachine_id (mach-{12hex}) on the top-level ProfileConfig, generated once by MachineIdProvider; stamps machine-specific memories so they never surface on other machines.
  • Registration: kcap/.mcp.json (Claude, with cwd) and kcap/.codex-mcp.json (no cwd — matches that file's precedent); built-in help (help-usage.txt, help-mcp.txt) and both READMEs updated.

Tests

Capacitor.Cli.Tests.Unit: 2077/2077 green (9 new: machine-id format/round-trip/back-compat + URL/body builders incl. snake_case, global, machine_specific rules).

Notes

  • Server counterpart: kcap-server branch alexeyzimarev/ai-1134-agent-memories (bump the submodule pointer to origin/main after this merges).
  • Follow-up: AI-1146 — wire kcap-memory into the kcap setup --codex allowlist + install docs.

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Jul 2, 2026

Copy link
Copy Markdown

AI-1134

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add MCP memory server and persisted machine id for machine-scoped memories

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add kcap mcp memory MCP stdio server with tools to search/read/write team memories.
• Persist a stable machine_id in profile config to tag machine-specific memories.
• Register the memory plugin for Claude and Codex; update help text, READMEs, and tests.
Diagram

graph TD
  A((Agent
Claude/Codex)) --> B["kcap mcp memory\n(MCP stdio)"] --> C{{"Capacitor Server\n/api/memories"}}
  B --> D["Repo detection\n(repo_hash)"]
  B --> E["MachineIdProvider"] --> F[("ProfileConfig\n(machine_id)")]
  B --> G["TokenStore/Auth\n(refresh on 401)"]

  subgraph Legend
    direction LR
    _a((Agent)) ~~~ _s["CLI/Service"] ~~~ _e{{External}} ~~~ _d[(Config/Store)]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Share a common MCP stdio server base with sessions/flows
  • ➕ Reduces duplication (stdio loop, JSON-RPC envelope helpers, error handling)
  • ➕ Keeps MCP server behavior consistent across subcommands
  • ➖ Refactor overhead across existing MCP servers
  • ➖ May slow delivery if the abstraction is not yet stable
2. Use typed request DTOs + source-generated JSON for bodies
  • ➕ Compile-time validation of required fields and naming
  • ➕ Easier evolution and documentation of request shapes
  • ➖ Requires extra JsonContext work to keep AOT trimming clean
  • ➖ More code churn when the server contract changes
3. Create authenticated HttpClient at startup (eager auth)
  • ➕ Early failure if not logged in / misconfigured base URL
  • ➕ Slightly simpler request path (no nullable client logic)
  • ➖ Auto-registered MCP servers would do network/token work even when unused
  • ➖ More noise/failure modes during agent session startup

Recommendation: The PR’s approach (lazy authenticated client creation, snake_case request bodies via JsonObject, and defensive JSON-RPC dispatch that preserves the stdio loop) is a good fit for auto-registered MCP servers and AOT constraints. The main follow-up worth considering is extracting a small shared MCP server helper to reduce repeated boilerplate across sessions/flows/memory without forcing a large refactor now.

Files changed (12) +544 / -1

Enhancement (4) +399 / -1
ProfileConfig.csPersist 'machine_id' in profile configuration +5/-0

Persist 'machine_id' in profile configuration

• Adds an optional 'machine_id' field to 'ProfileConfig' with JSON name 'machine_id', enabling a stable local machine identity for machine-scoped memories.

src/Capacitor.Cli.Core/Config/ProfileConfig.cs

MachineIdProvider.csAdd MachineIdProvider to generate and persist stable machine ids +17/-0

Add MachineIdProvider to generate and persist stable machine ids

• Introduces a helper that generates 'mach-{12hex}' ids and stores them in profile config on first use, returning the persisted value thereafter.

src/Capacitor.Cli.Core/MachineIdProvider.cs

McpMemoryServer.csImplement 'kcap mcp memory' MCP stdio server with six memory tools +373/-0

Implement 'kcap mcp memory' MCP stdio server with six memory tools

• Adds a new MCP server that handles initialize/tools/list/tools/call over stdio and proxies tool calls to the server’s '/api/memories' endpoints. Resolves repo hash from cwd and machine id from persisted config to scope/bias queries, builds snake_case request bodies with 'JsonObject', and performs a one-shot token refresh retry on 401 while keeping the JSON-RPC loop resilient to bad inputs and unexpected errors.

src/Capacitor.Cli/Commands/McpMemoryServer.cs

Program.csWire 'kcap mcp memory' into CLI routing and usage text +4/-1

Wire 'kcap mcp memory' into CLI routing and usage text

• Updates the 'kcap mcp' usage output and adds a 'memory' case to dispatch to 'McpMemoryServer.RunAsync'.

src/Capacitor.Cli/Program.cs

Tests (2) +90 / -0
MachineIdTests.csAdd tests for machine id format and config round-trip/back-compat +32/-0

Add tests for machine id format and config round-trip/back-compat

• Verifies generated id format, confirms 'machine_id' serializes/deserializes correctly via the source-generated JSON context, and ensures older configs deserialize with 'MachineId == null'.

test/Capacitor.Cli.Tests.Unit/MachineIdTests.cs

McpMemoryServerTests.csAdd unit tests for memory URL/body builders and tool registration +58/-0

Add unit tests for memory URL/body builders and tool registration

• Covers search/get URL composition (including repo/machine context), save body behavior for 'global' and 'machine_specific', and asserts the tools list exposes six tools including 'save_memory'.

test/Capacitor.Cli.Tests.Unit/McpMemoryServerTests.cs

Documentation (4) +45 / -0
README.mdDocument the new Memory MCP server and its tool set +21/-0

Document the new Memory MCP server and its tool set

• Adds a top-level mention of 'kcap mcp memory' and a dedicated section describing its purpose, tools, and repo/machine scoping behavior. Clarifies Claude vs Codex registration paths.

README.md

README.mdExpose 'kcap-memory' tools in plugin README +15/-0

Expose 'kcap-memory' tools in plugin README

• Documents the 'kcap-memory' tool list and describes its repo- and machine-aware behavior for scoping saves and biasing search.

kcap/README.md

help-mcp.txtAdd 'mcp memory' subcommand help +8/-0

Add 'mcp memory' subcommand help

• Extends built-in help to list the new 'memory' MCP server and briefly explains its six tools, scoping behavior, and login requirement.

src/Capacitor.Cli.Core/Resources/help-mcp.txt

help-usage.txtList 'kcap mcp memory' in CLI usage help +1/-0

List 'kcap mcp memory' in CLI usage help

• Adds 'mcp memory' to the MCP servers list, summarizing the available memory operations.

src/Capacitor.Cli.Core/Resources/help-usage.txt

Other (2) +10 / -0
.codex-mcp.jsonRegister 'kcap-memory' for Codex native plugin loader +4/-0

Register 'kcap-memory' for Codex native plugin loader

• Adds a 'kcap-memory' entry pointing to 'kcap mcp memory' so Codex can discover the MCP server via its JSON plugin descriptor.

kcap/.codex-mcp.json

.mcp.jsonAuto-register 'kcap-memory' for Claude Code +6/-0

Auto-register 'kcap-memory' for Claude Code

• Adds a 'kcap-memory' MCP entry using the project directory as 'cwd' and a short description, enabling automatic Claude Code registration via the plugin file.

kcap/.mcp.json

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials

Grey Divider


Action required

1. Repo-scoped save can globalize 🐞 Bug ≡ Correctness
Description
save_memory defaults to repo-scoped, but if repo detection fails (cwdRepoHash is null)
BuildSaveBody sends repo_hash: null even when global is false, which can unintentionally
create a global (repo-independent) memory. This can happen in non-git directories or when the remote
URL can’t be parsed into owner/repo, violating the tool’s documented default scoping.
Code

src/Capacitor.Cli/Commands/McpMemoryServer.cs[242]

+            ["repo_hash"]         = global ? null : cwdRepoHash,
Evidence
The code explicitly returns null for cwdRepoHash on detection failures, and then uses that null to
populate repo_hash unless global:true was set; this contradicts the tool contract that repo
scoping is the default behavior.

src/Capacitor.Cli/Commands/McpMemoryServer.cs[99-110]
src/Capacitor.Cli/Commands/McpMemoryServer.cs[228-247]
src/Capacitor.Cli/Commands/McpMemoryServer.cs[342-353]
src/Capacitor.Cli/RepositoryDetection.cs[83-129]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`save_memory` is documented as repo-scoped by default, but the request builder currently falls back to `repo_hash = null` whenever `cwdRepoHash` is unavailable. If the server interprets `repo_hash: null` as global, this broadens scope unintentionally.

## Issue Context
Repo hash resolution is best-effort and can legitimately return null (non-git cwd, missing/unknown remote, parse failures).

## Fix Focus Areas
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[99-110]
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[228-247]
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[342-353]

## Suggested fix
- In `BuildSaveBody`, if `global == false` and `cwdRepoHash == null`, throw `ArgumentException` with an actionable message (e.g. "Unable to resolve repo for cwd; run inside a git repo or pass global:true").
- (Optional) Consider adding an explicit tool argument like `repo_hash` for callers that can provide it, but only if that matches product intent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. machine_specific may be dropped 🐞 Bug ≡ Correctness
Description
If machine id resolution fails, ResolveMachineIdAsync returns null and BuildSaveBody will send
machine_tag: null even when machine_specific: true, silently saving a non-machine-specific
memory. This breaks the expected isolation of machine-specific memories and can cause them to
surface on other machines.
Code

src/Capacitor.Cli/Commands/McpMemoryServer.cs[243]

+            ["machine_tag"]       = machineSpecific ? machineId : null,
Evidence
ResolveMachineIdAsync converts any failure into null, and BuildSaveBody conditionally assigns
machine_tag to that potentially-null value when machine_specific is true.

src/Capacitor.Cli/Commands/McpMemoryServer.cs[112-114]
src/Capacitor.Cli/Commands/McpMemoryServer.cs[232-244]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `machine_specific` is requested, the server call must include a non-null machine tag. Today, failures in machine-id load/create are swallowed and `machine_tag` becomes null.

## Issue Context
`ResolveMachineIdAsync` catches all exceptions and returns null; `BuildSaveBody` uses `machineId` without enforcing non-null when `machine_specific` is set.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[112-114]
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[228-247]

## Suggested fix
- In `BuildSaveBody`, if `machine_specific == true` and `machineId == null`, throw `ArgumentException` ("machine_specific requires a persisted machine id; fix config permissions or retry").
- (Optional) In `ResolveMachineIdAsync`, log a one-line stderr warning when it fails so the behavior isn’t silent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Unvalidated persisted machine_id 🐞 Bug ☼ Reliability
Description
MachineIdProvider.GetOrCreateAsync trusts any non-whitespace machine_id from config without
validating the expected mach-{12hex} format, so a corrupted/manual value will be propagated into
requests. This can lead to server-side validation failures or incorrect machine scoping behavior.
Code

src/Capacitor.Cli.Core/MachineIdProvider.cs[R9-15]

+    public static async Task<string> GetOrCreateAsync() {
+        var config = await AppConfig.LoadProfileConfig();
+        if (!string.IsNullOrWhiteSpace(config.MachineId)) return config.MachineId;
+
+        var id = Generate();
+        await AppConfig.SaveProfileConfig(config with { MachineId = id });
+        return id;
Evidence
Generation is constrained to a specific format and tested, but the persisted value path only checks
for whitespace and returns whatever is in config, even if it doesn’t match the expected shape.

src/Capacitor.Cli.Core/MachineIdProvider.cs[5-16]
test/Capacitor.Cli.Tests.Unit/MachineIdTests.cs[7-13]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The code assumes `machine_id` has a stable format (and tests enforce generation format), but `GetOrCreateAsync` returns any non-empty persisted value as-is.

## Issue Context
`MachineIdProvider.Generate()` produces `mach-` + 12 hex characters, and unit tests assert this format; persisted config values are not checked.

## Fix Focus Areas
- src/Capacitor.Cli.Core/MachineIdProvider.cs[5-16]
- test/Capacitor.Cli.Tests.Unit/MachineIdTests.cs[7-13]

## Suggested fix
- Add a small validator (regex or manual check) for `^mach-[0-9a-f]{12}$`.
- In `GetOrCreateAsync`, if `config.MachineId` is present but invalid, generate a new id and persist it (or surface a clear error if you prefer not to auto-heal).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

["content"] = Req("content"),
["kind"] = Req("kind"),
["team"] = args?["team"]?.GetValue<string>(),
["repo_hash"] = global ? null : cwdRepoHash,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Repo-scoped save can globalize 🐞 Bug ≡ Correctness

save_memory defaults to repo-scoped, but if repo detection fails (cwdRepoHash is null)
BuildSaveBody sends repo_hash: null even when global is false, which can unintentionally
create a global (repo-independent) memory. This can happen in non-git directories or when the remote
URL can’t be parsed into owner/repo, violating the tool’s documented default scoping.
Agent Prompt
## Issue description
`save_memory` is documented as repo-scoped by default, but the request builder currently falls back to `repo_hash = null` whenever `cwdRepoHash` is unavailable. If the server interprets `repo_hash: null` as global, this broadens scope unintentionally.

## Issue Context
Repo hash resolution is best-effort and can legitimately return null (non-git cwd, missing/unknown remote, parse failures).

## Fix Focus Areas
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[99-110]
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[228-247]
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[342-353]

## Suggested fix
- In `BuildSaveBody`, if `global == false` and `cwdRepoHash == null`, throw `ArgumentException` with an actionable message (e.g. "Unable to resolve repo for cwd; run inside a git repo or pass global:true").
- (Optional) Consider adding an explicit tool argument like `repo_hash` for callers that can provide it, but only if that matches product intent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

["kind"] = Req("kind"),
["team"] = args?["team"]?.GetValue<string>(),
["repo_hash"] = global ? null : cwdRepoHash,
["machine_tag"] = machineSpecific ? machineId : null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Machine_specific may be dropped 🐞 Bug ≡ Correctness

If machine id resolution fails, ResolveMachineIdAsync returns null and BuildSaveBody will send
machine_tag: null even when machine_specific: true, silently saving a non-machine-specific
memory. This breaks the expected isolation of machine-specific memories and can cause them to
surface on other machines.
Agent Prompt
## Issue description
When `machine_specific` is requested, the server call must include a non-null machine tag. Today, failures in machine-id load/create are swallowed and `machine_tag` becomes null.

## Issue Context
`ResolveMachineIdAsync` catches all exceptions and returns null; `BuildSaveBody` uses `machineId` without enforcing non-null when `machine_specific` is set.

## Fix Focus Areas
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[112-114]
- src/Capacitor.Cli/Commands/McpMemoryServer.cs[228-247]

## Suggested fix
- In `BuildSaveBody`, if `machine_specific == true` and `machineId == null`, throw `ArgumentException` ("machine_specific requires a persisted machine id; fix config permissions or retry").
- (Optional) In `ResolveMachineIdAsync`, log a one-line stderr warning when it fails so the behavior isn’t silent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +9 to +15
public static async Task<string> GetOrCreateAsync() {
var config = await AppConfig.LoadProfileConfig();
if (!string.IsNullOrWhiteSpace(config.MachineId)) return config.MachineId;

var id = Generate();
await AppConfig.SaveProfileConfig(config with { MachineId = id });
return id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Unvalidated persisted machine_id 🐞 Bug ☼ Reliability

MachineIdProvider.GetOrCreateAsync trusts any non-whitespace machine_id from config without
validating the expected mach-{12hex} format, so a corrupted/manual value will be propagated into
requests. This can lead to server-side validation failures or incorrect machine scoping behavior.
Agent Prompt
## Issue description
The code assumes `machine_id` has a stable format (and tests enforce generation format), but `GetOrCreateAsync` returns any non-empty persisted value as-is.

## Issue Context
`MachineIdProvider.Generate()` produces `mach-` + 12 hex characters, and unit tests assert this format; persisted config values are not checked.

## Fix Focus Areas
- src/Capacitor.Cli.Core/MachineIdProvider.cs[5-16]
- test/Capacitor.Cli.Tests.Unit/MachineIdTests.cs[7-13]

## Suggested fix
- Add a small validator (regex or manual check) for `^mach-[0-9a-f]{12}$`.
- In `GetOrCreateAsync`, if `config.MachineId` is present but invalid, generate a new id and persist it (or surface a clear error if you prefer not to auto-heal).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant