Skip to content

Commit dc16ca7

Browse files
committed
feat(mcp): add --project flag and ARCHCORE_PROJECT_ROOT env override
1 parent 37a802a commit dc16ca7

8 files changed

Lines changed: 347 additions & 26 deletions

File tree

.archcore/cli/mcp-token-optimization.idea.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,23 @@ status: draft
77

88
Reduce token consumption of the MCP server by deduplicating tool descriptions, trimming redundant response fields, and consolidating disambiguation rules — without sacrificing document classification accuracy.
99

10-
Current baseline: **~5,600 tokens/session** fixed overhead (system instructions ~2,900 + tool schemas ~2,100 + session context ~600). Realistic savings: **800–1,300 tokens/session (15–23%)**.
10+
Original baseline: **~5,600 tokens/session** fixed overhead (system instructions ~2,900 + tool schemas ~2,100 + session context ~600). Realistic savings: **800–1,300 tokens/session (15–23%)**.
11+
12+
## Status
13+
14+
**Partially shipped** in commit `05d1af3 fix: safe token optimizations in MCP tools` (2026-05-12). The "safe" subset — changes that do not alter the response contract or risk type-selection accuracy — has landed. The riskier items (response-shape changes and full system-prompt consolidation) remain open and are tracked below.
15+
16+
### Shipped (safe)
17+
18+
- **Compressed `create_document` description** (partial Item 1). The 18 document types are still enumerated in the tool description, but each entry is now one or two lines instead of a full multi-line section list. Section detail is delegated to the auto-generated template. The `tags` and `content` parameter descriptions were also tightened, with longer reference text pushed to server instructions. Net effect: roughly half the tokens of the original create_document schema, without removing type guidance.
19+
- **`nearby_documents` cap** (Item 5). `populateNearbyDocuments` in `internal/mcp/tools/create_document.go` now sorts results alphabetically and caps at `maxNearbyDocuments = 5`. Bounds the response size in large directories.
20+
21+
### Not shipped (deferred)
22+
23+
- **Item 1 (full)** — Removing the type list from `create_document` entirely and pointing to system instructions. The compressed version was chosen instead because tool-schema-weighted models still benefit from in-schema type names. Full deduplication remains a candidate if A/B testing shows the compressed list is also expendable.
24+
- **Item 2 — Remove `filename` + `slug` from `list_documents` response.** Both fields are still in `LocalDocument` (`internal/mcp/tools/common.go`). Mechanically derivable from `path`. ~430 tokens / 50 docs.
25+
- **Item 3 — Strip frontmatter from `content` in `get_document` response.** Still returned verbatim. Contract change — needs a migration plan (or a `raw` opt-in parameter) before shipping. ~10–14 tokens/call.
26+
- **Item 4 — Consolidate requirements tracks + layers + type selection in `mcpServerInstructions`.** `internal/mcp/server.go` was not touched in `05d1af3`. ~150–200 tokens.
1127

1228
## Value
1329

@@ -16,7 +32,7 @@ Current baseline: **~5,600 tokens/session** fixed overhead (system instructions
1632
- More headroom in context window for actual user work
1733
- No degradation in document quality or type selection accuracy
1834

19-
## Current Token Budget
35+
## Current Token Budget (pre-`05d1af3` baseline)
2036

2137
| Component | Tokens | % |
2238
|---|---|---|
@@ -26,6 +42,8 @@ Current baseline: **~5,600 tokens/session** fixed overhead (system instructions
2642
| Session-start context (doc list + relations) | ~600 | 10% |
2743
| **Total baseline** | **~5,600** | **100%** |
2844

45+
Post-`05d1af3` figures have not been re-measured. Re-baseline before deciding whether the remaining items are worth pursuing.
46+
2947
### System Prompt Breakdown
3048

3149
| Section | Tokens | % of instructions |
@@ -47,24 +65,24 @@ Minimal prompts yield 25–40% error rate (wrong type, missing sections). Curren
4765

4866
## Possible Implementation
4967

50-
### 1. Deduplicate `create_document` description (~500–700 tokens saved)
51-
The tool description re-lists all 18 types with required sections (~700 tokens) that already exist in system instructions. Replace with a reference: "See system instructions for required sections per type."
68+
### 1. Deduplicate `create_document` description (~500–700 tokens saved)*partial: compressed in `05d1af3`*
69+
The tool description re-lists all 18 types with required sections (~700 tokens) that already exist in system instructions. The compressed shipped version keeps a one-line summary per type; a future change could replace with a single reference: "See system instructions for required sections per type."
5270

53-
### 2. Remove `filename` + `slug` from `list_documents` response (~430 tokens per 50 docs)
71+
### 2. Remove `filename` + `slug` from `list_documents` response (~430 tokens per 50 docs)*open*
5472
Both are mechanically derivable from `path`. Strip from JSON response.
5573

56-
### 3. Strip frontmatter from `content` in `get_document` response (~10–14 tokens/call)
74+
### 3. Strip frontmatter from `content` in `get_document` response (~10–14 tokens/call)*open*
5775
`title` and `status` are already decoded as separate JSON fields. The raw frontmatter inside `content` is redundant.
5876

59-
### 4. Consolidate requirements tracks + layers + type selection (~150–200 tokens)
77+
### 4. Consolidate requirements tracks + layers + type selection (~150–200 tokens)*open*
6078
Three overlapping blocks cover similar disambiguation ground. Merge into a single compact reference table.
6179

62-
### 5. Cap `nearby_documents` in `create_document` response (up to ~286 tokens saved)
63-
In directories with 20+ documents, the hint array grows unbounded. Cap at 5 entries.
80+
### 5. Cap `nearby_documents` in `create_document` response (up to ~286 tokens saved)*shipped in `05d1af3`*
81+
Capped at 5 entries, sorted alphabetically.
6482

6583
## Risks and Constraints
6684

67-
- **Deduplication risk**: Some LLMs weight tool-level descriptions higher than system instructions. Removing type info from `create_document` may reduce accuracy for models that prioritize tool schemas over system prompt. Needs A/B testing.
85+
- **Deduplication risk**: Some LLMs weight tool-level descriptions higher than system instructions. Removing type info from `create_document` may reduce accuracy for models that prioritize tool schemas over system prompt. Needs A/B testing. The compressed form shipped in `05d1af3` is a hedge against this risk.
6886
- **Stripping frontmatter from get_document**: Changes the contract — agents that parse `content` expecting frontmatter will break. Requires migration path or a `raw` parameter.
6987
- **Consolidating disambiguation rules**: The current verbose format is optimized for LLM comprehension. A compact table may reduce readability for the model. Test with edge cases (brs vs brd, strs vs urd).
70-
- **Session-start context scales with repo size**: The document list and relations summary grow linearly. This analysis covers the fixed prompt; the scaling overhead is a separate concern.
88+
- **Session-start context scales with repo size**: The document list and relations summary grow linearly. This analysis covers the fixed prompt; the scaling overhead is a separate concern.

.archcore/integrations/agent-hooks-integration.guide.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,29 @@ Cline stores MCP config in VS Code `globalStorage`, not in project files. To add
218218
1. Open Cline MCP settings in VS Code
219219
2. Add an MCP server with command `archcore` and args `["mcp"]`
220220

221+
## Pointing the MCP Server at a Specific Project Root
222+
223+
By default `archcore mcp` serves documents from the current working directory. Some editor integrations launch the binary from a directory that isn't the workspace root (e.g. a desktop app's install dir, or a Cline globalStorage profile). In those cases the server may see an empty/wrong project.
224+
225+
Two overrides are available:
226+
227+
- **Flag**: `archcore mcp --project /absolute/path/to/repo`
228+
- **Environment**: `ARCHCORE_PROJECT_ROOT=/absolute/path/to/repo archcore mcp`
229+
230+
Precedence: `--project` > `ARCHCORE_PROJECT_ROOT` > current working directory.
231+
232+
The path must point at an existing directory (it does not need to contain `.archcore/` yet — the server still starts and exposes `init_project`).
233+
234+
Example for an agent that needs an absolute workspace path:
235+
236+
```json
237+
{
238+
"mcpServers": {
239+
"archcore": { "command": "archcore", "args": ["mcp", "--project", "/Users/me/code/my-repo"] }
240+
}
241+
}
242+
```
243+
221244
## Invalid Config Recovery
222245

223246
When archcore reads a config file that contains invalid JSON, it creates a `.bak` backup before proceeding with a fresh config. This prevents data loss while keeping the installation non-blocking.
@@ -233,6 +256,14 @@ See [Backup Invalid Configs](backup-invalid-configs.adr.md) for the full decisio
233256
- The **MCP server** (`archcore mcp`) starts fine without `.archcore/` — it exposes `init_project` so the agent can bootstrap the directory in-session. If the agent sees an empty `list_documents` result and wants to create documents, it should call `init_project` first.
234257
- **Hooks** and the `archcore hooks/mcp install` commands still require an initialized project. Run `archcore init` first, or ask the agent to call `init_project`.
235258

259+
### MCP server is serving the wrong directory
260+
261+
Symptoms: `list_documents` returns an empty array even though the workspace clearly has `.archcore/` documents, or `init_project` would create the directory in an unexpected location.
262+
263+
Cause: the agent launched `archcore mcp` from a working directory that isn't your workspace.
264+
265+
Fix: pass `--project` explicitly in the agent's MCP config, or set `ARCHCORE_PROJECT_ROOT` in the agent's environment. See **Pointing the MCP Server at a Specific Project Root** above.
266+
236267
### Agent not detected
237268

238269
Check that the agent's marker directory exists in your project root. You can also target a specific agent with `--agent`:

.archcore/mcp/mcp-server-starts-without-archcore-dir.adr.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Two user flows were affected:
1616

1717
## Decision
1818

19-
1. **`archcore mcp` starts unconditionally.** The `.archcore/` existence check in `cmd/mcp.go` is removed. The server always boots on stdio and prints a hint in the startup banner when the directory is missing.
19+
1. **`archcore mcp` starts unconditionally for the default (cwd) project root.** The `.archcore/` existence check in `cmd/mcp.go` is removed. The server always boots on stdio and prints a hint in the startup banner when the directory is missing.
2020
2. **Add an `init_project` MCP tool** (`internal/mcp/tools/init_project.go`). It creates `.archcore/` and writes `settings.json` from within the MCP server itself. It accepts `language`, `sync_mode`, and `archcore_url` and is idempotent — calling it on an already-initialized project returns the existing settings without overwriting them.
2121
3. **Server instructions document the bootstrap flow.** The system prompt embedded in `internal/mcp/server.go` (`mcpServerInstructions`) explicitly tells the agent: "if `list_documents` returns empty AND the user wants to create documents, call `init_project` once, then proceed."
2222
4. **Other tools continue to assume `.archcore/` exists.** `create_document`, `list_documents`, `update_document`, etc. are not changed to auto-init. The only tool safe to call on an uninitialized project is `init_project`.
@@ -40,3 +40,12 @@ Negative / constraints:
4040
- The MCP server now has a "degraded" mode (running, but most tools will fail). The startup banner's hint compensates, but agents that ignore system prompts may attempt `create_document` before `init_project` and see confusing errors. Mitigated by the explicit instruction block in `mcpServerInstructions`.
4141
- `init_project` must stay idempotent. A regression that clobbers existing `settings.json` on the second call would destroy user configuration. Covered by `TestHandleInitProject_Idempotent`.
4242
- Any future MCP tool that *also* safely works on an uninitialized project must be documented in the server instructions alongside `init_project`. Today that list is just `init_project`.
43+
44+
### Scope of the "unconditional start" property
45+
46+
The unconditional-start guarantee applies to the **default project root** (cwd) and to the case where `.archcore/` is missing inside an existing project directory. It does **not** override the project-root resolution layer:
47+
48+
- When `--project <path>` is passed to `archcore mcp`, or `ARCHCORE_PROJECT_ROOT` is set, the resolver (`cmd/mcp_root.go`) requires the path to exist and to be a directory. A missing or non-directory path causes the command to exit before the server starts.
49+
- This is intentional: if the user explicitly names a project root, silently falling back to cwd would be more surprising than failing fast. `.archcore/` *inside* the resolved root may still be absent — the server still starts in that case and exposes `init_project`.
50+
51+
In short: the server tolerates a missing `.archcore/`, but does not tolerate a missing project directory the user explicitly named.

.archcore/release/how-to-release.guide.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,41 @@ status: accepted
3535

3636
The GitHub Actions workflow (`.github/workflows/release.yml`) triggers automatically. It will:
3737
- Run `go test ./...`
38-
- Build binaries for darwin/linux × amd64/arm64
38+
- Build binaries for darwin/linux/windows × amd64/arm64 (6 platforms)
3939
- Create a GitHub Release with archives and `checksums.txt`
4040

4141
Watch progress at: `https://github.com/archcore-ai/cli/actions`
4242

4343
5. **Verify the release**
4444

4545
```bash
46-
# Check the GitHub Release page has all 5 assets (4 archives + checksums.txt)
46+
# Check the GitHub Release page has all 7 assets (6 archives + checksums.txt)
4747
gh release view v1.0.0
4848

49-
# Test the install script
49+
# Test the install script (macOS/Linux)
5050
ARCHCORE_VERSION=v1.0.0 curl -fsSL https://raw.githubusercontent.com/archcore-ai/cli/main/install.sh | bash
5151

52+
# Test the install script (Windows PowerShell)
53+
$env:ARCHCORE_VERSION="v1.0.0"; irm https://raw.githubusercontent.com/archcore-ai/cli/main/install.ps1 | iex
54+
5255
# Verify the installed binary
5356
archcore --version
5457
# Expected: archcore 1.0.0 (commit: <sha>)
5558
```
5659

5760
## Verification
5861

59-
- GitHub Release page shows 4 `.tar.gz` archives + `checksums.txt`
62+
- GitHub Release page shows 6 archives (4 `.tar.gz` for darwin/linux amd64+arm64, 2 `.zip` for windows amd64+arm64) plus `checksums.txt`
6063
- `archcore --version` on installed binary shows correct version and commit
61-
- Install script succeeds on a clean machine
64+
- Install script succeeds on a clean macOS/Linux machine
65+
- `install.ps1` succeeds on a clean Windows machine
66+
67+
See [Release Infrastructure Overview](release-infrastructure.doc.md) for the full build matrix, artifact naming, and update paths.
6268

6369
## Common Issues
6470

6571
- **Workflow fails at test step** — Fix the tests on `main`, delete the tag (`git push origin :v1.0.0 && git tag -d v1.0.0`), then re-tag after fixing.
6672
- **GoReleaser fails** — Check `.goreleaser.yaml` syntax. Run `goreleaser check` locally if available.
6773
- **Wrong commit tagged** — Delete the remote tag, re-tag the correct commit, and push again.
68-
- **install.sh can't find the release** — Ensure the tag follows the `v*` pattern (e.g. `v1.0.0`, not `1.0.0`).
74+
- **install.sh / install.ps1 can't find the release** — Ensure the tag follows the `v*` pattern (e.g. `v1.0.0`, not `1.0.0`).
75+
- **Windows zips missing from release** — Confirm `.goreleaser.yaml` `format_overrides` still maps `windows` to `zip`; otherwise GoReleaser will fall back to `.tar.gz` and `install.ps1` won't find the expected archives.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ Archcore does not require a hosted service. The CLI runs a local stdio MCP serve
301301
archcore mcp
302302
```
303303

304+
By default `archcore mcp` serves documents from the current directory. Pass `--project /path/to/repo` (or set `ARCHCORE_PROJECT_ROOT`) to point it elsewhere — useful when the server is launched from a directory that isn't your workspace (for example, by an editor integration).
305+
304306
Wire it into Claude Code:
305307

306308
```bash

cmd/mcp.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,65 @@ import (
1414
)
1515

1616
func newMCPCmd() *cobra.Command {
17+
var projectFlag string
18+
1719
cmd := &cobra.Command{
1820
Use: "mcp",
1921
Short: "MCP stdio server for archcore documents",
2022
Long: "Starts an MCP (Model Context Protocol) stdio server that exposes archcore document tools.",
2123
RunE: func(cmd *cobra.Command, args []string) error {
22-
cwd, err := os.Getwd()
24+
baseDir, err := resolveProjectRoot(projectFlag, os.Getenv("ARCHCORE_PROJECT_ROOT"))
2325
if err != nil {
2426
return err
2527
}
2628

2729
fmt.Fprintln(os.Stderr, display.WelcomeBanner())
2830
fmt.Fprintln(os.Stderr)
29-
if !config.DirExists(cwd) {
31+
if !config.DirExists(baseDir) {
3032
fmt.Fprintln(os.Stderr, display.Dim.Render(" MCP server running on stdio (uninitialized project — only init_project tool is useful until the agent initializes .archcore/)..."))
3133
} else {
3234
fmt.Fprintln(os.Stderr, display.Dim.Render(" MCP server running on stdio..."))
3335
}
3436

35-
return mcpserver.RunStdio(cwd)
37+
return mcpserver.RunStdio(baseDir)
3638
},
3739
}
3840

41+
cmd.Flags().StringVar(&projectFlag, "project", "",
42+
"project root containing .archcore/ (default: current directory; env: ARCHCORE_PROJECT_ROOT)")
43+
3944
cmd.AddCommand(newMCPInstallCmd())
4045
return cmd
4146
}
4247

4348
func newMCPInstallCmd() *cobra.Command {
44-
var agentFlag string
49+
var (
50+
agentFlag string
51+
projectFlag string
52+
)
4553

4654
cmd := &cobra.Command{
4755
Use: "install",
4856
Short: "Install MCP server config for coding agents",
4957
RunE: func(cmd *cobra.Command, args []string) error {
50-
cwd, err := os.Getwd()
58+
baseDir, err := resolveProjectRoot(projectFlag, os.Getenv("ARCHCORE_PROJECT_ROOT"))
5159
if err != nil {
5260
return err
5361
}
54-
if !config.DirExists(cwd) {
62+
if !config.DirExists(baseDir) {
5563
return errors.New(".archcore/ not found — run 'archcore init' first")
5664
}
5765

5866
if agentFlag != "" {
59-
return runMCPInstallForAgent(cwd, agents.AgentID(agentFlag))
67+
return runMCPInstallForAgent(baseDir, agents.AgentID(agentFlag))
6068
}
61-
return runMCPInstallAutoDetect(cwd)
69+
return runMCPInstallAutoDetect(baseDir)
6270
},
6371
}
6472

6573
cmd.Flags().StringVar(&agentFlag, "agent", "", "install for a specific agent (e.g. cursor, gemini-cli)")
74+
cmd.Flags().StringVar(&projectFlag, "project", "",
75+
"project root containing .archcore/ (default: current directory; env: ARCHCORE_PROJECT_ROOT)")
6676
return cmd
6777
}
6878

cmd/mcp_root.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
)
10+
11+
// resolveProjectRoot picks the project root from (in order):
12+
// 1. flagValue if non-empty
13+
// 2. envValue if non-empty
14+
// 3. os.Getwd()
15+
//
16+
// The returned path is absolute. Errors if the source cannot be made absolute,
17+
// does not exist, or is not a directory.
18+
//
19+
// envValue is passed in (not read inside) so the caller controls precedence
20+
// visibly and tests do not have to mutate process env.
21+
func resolveProjectRoot(flagValue, envValue string) (string, error) {
22+
var source string
23+
switch {
24+
case flagValue != "":
25+
source = flagValue
26+
case envValue != "":
27+
source = envValue
28+
default:
29+
cwd, err := os.Getwd()
30+
if err != nil {
31+
return "", fmt.Errorf("failed to determine working directory: %w", err)
32+
}
33+
source = cwd
34+
}
35+
36+
abs, err := filepath.Abs(source)
37+
if err != nil {
38+
return "", fmt.Errorf("failed to resolve project root %q: %w", source, err)
39+
}
40+
41+
info, err := os.Stat(abs)
42+
if err != nil {
43+
if errors.Is(err, fs.ErrNotExist) {
44+
return "", fmt.Errorf("project root does not exist: %q", abs)
45+
}
46+
return "", fmt.Errorf("failed to stat project root %q: %w", abs, err)
47+
}
48+
49+
if !info.IsDir() {
50+
return "", fmt.Errorf("project root is not a directory: %q", abs)
51+
}
52+
53+
return abs, nil
54+
}

0 commit comments

Comments
 (0)