Skip to content

Commit 90f66e0

Browse files
committed
chore(mcp): simplify server rendering and document wiki sync
Remove per-target MCP transport overrides, make conditions generic key/value matches against global data, and simplify Cursor/OpenCode templates to canonical local/remote configs. Add user-facing MCP docs and a wiki sync workflow, plus update CI to run source tests through pre-commit after mise install. Made-with: Cursor
1 parent e6293cc commit 90f66e0

13 files changed

Lines changed: 187 additions & 194 deletions

File tree

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: update-mcp-servers
3-
description: Update canonical MCP server definitions for this chezmoi repo, including target overrides and template rendering checks. Use when editing mcp-servers.yaml, Cursor MCP template output, or OpenCode MCP config generation.
3+
description: Update canonical MCP server definitions for this chezmoi repo and template rendering checks. Use when editing mcp-servers.yaml, Cursor MCP template output, or OpenCode MCP config generation.
44
---
55

66
# Update MCP Servers
@@ -13,14 +13,14 @@ Use this skill when changing MCP server configuration in this repo.
1313
- Cursor render template: `home/dot_cursor/mcp.json.tmpl`
1414
- OpenCode render template: `home/dot_config/opencode/opencode.jsonc.tmpl`
1515

16-
Treat `mcp-servers.yaml` as the single source of truth. Keep target-specific config only as explicit overrides.
16+
Treat `mcp-servers.yaml` as the single source of truth.
1717

1818
## Canonical schema
1919

2020
Each entry in `mcpServers` should follow this shape:
2121

2222
- `id`, `enabled`
23-
- optional `conditions.requirePrivateFalse`
23+
- optional `conditions` object, each key/value is matched against global chezmoi data
2424
- `targets.opencode.enabled`
2525
- `targets.cursor.enabled`
2626
- either `local` or `remote`
@@ -30,25 +30,18 @@ Local shape:
3030
- `local.command` string
3131
- `local.args` array
3232
- optional `local.env`
33-
- optional `local.appendVaultPath`
33+
- optional `local.appendDataArgs` array of chezmoi data keys to append as args
3434

3535
Remote shape:
3636

3737
- `remote.url`
3838
- optional `remote.transport`
3939
- optional `remote.headers`
4040

41-
Optional target override shape:
42-
43-
- `targets.opencode.local` or `targets.opencode.remote`
44-
- `targets.cursor.local` or `targets.cursor.remote`
45-
46-
Only include override keys that actually differ from canonical values.
47-
4841
## Editing rules
4942

50-
1. Prefer canonical values over per-target overrides.
51-
2. Keep overrides minimal, only for true target differences.
43+
1. Keep a single canonical `local` or `remote` config per server.
44+
2. Do not re-introduce per-target transport overrides.
5245
3. Do not re-introduce duplicate fields like parallel `cursorLocal` and `local`.
5346
4. Keep command execution via `mise` where applicable.
5447

@@ -61,4 +54,4 @@ After MCP changes:
6154
3. Validate the rendered Cursor MCP output is valid JSON.
6255
4. Confirm expected server entries and args in rendered output.
6356

64-
If templates fail due to missing keys, use `hasKey` checks and safe defaults in templates, do not force duplication in YAML.
57+
Templates should trust schema-required fields. Use `hasKey` checks only for optional fields.

.github/workflows/test.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,19 @@ jobs:
1616
steps:
1717
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
1818

19+
- name: Install mise
20+
run: |
21+
curl https://mise.run | sh
22+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
23+
1924
- name: Install dependencies
2025
run: |
2126
sudo apt-get update -q
2227
sudo apt-get install -yq bats
2328
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b /usr/local/bin
2429
25-
- name: Run source tests
26-
run: bats tests/source/
30+
- name: Install toolchain
31+
run: mise install
32+
33+
- name: Run tests via pre-commit
34+
run: mise x -- pre-commit run --all-files --show-diff-on-failure

.github/workflows/wiki-sync.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
name: Wiki Sync
3+
4+
on:
5+
push:
6+
branches: [main]
7+
paths:
8+
- docs/**
9+
- .github/workflows/wiki-sync.yml
10+
workflow_dispatch: null
11+
12+
permissions:
13+
contents: write
14+
15+
concurrency:
16+
group: ${{ github.workflow }}-${{ github.ref }}
17+
cancel-in-progress: true
18+
19+
jobs:
20+
sync-wiki:
21+
runs-on: ubuntu-24.04
22+
steps:
23+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
24+
25+
# NOTE: The repository wiki must be initialized first (create one wiki page manually).
26+
- name: Sync docs to wiki
27+
uses: Andrew-Chen-Wang/github-wiki-action@64efa0a9436db17670a2259e0ac249d6f08bb352 # v5.0.4
28+
with:
29+
path: docs/
30+
preprocess: true
31+
disable-empty-commits: true

docs/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Dotfiles Documentation
2+
3+
This wiki mirrors the `docs/` directory from the dotfiles repository.
4+
5+
## Pages
6+
7+
- [MCP Server Configuration](mcp-servers)
8+
- [Brew Review](brew-review)
9+
- [Keybinds](keybinds)
10+
- [Secrets](secrets)
11+
- [YubiKey](yubikey)

docs/mcp-servers.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# MCP Server Configuration
2+
3+
This repo defines MCP servers in one canonical file:
4+
5+
- `home/.chezmoidata/mcp-servers.yaml`
6+
7+
That data is rendered into:
8+
9+
- Cursor: `home/dot_cursor/mcp.json.tmpl`
10+
- OpenCode: `home/dot_config/opencode/opencode.jsonc.tmpl`
11+
12+
## Design Rules
13+
14+
- Single transport config per server, use either `local` or `remote`.
15+
- No per-target transport overrides under `targets`.
16+
- `targets` only controls enablement per client (`cursor` and `opencode`).
17+
- Template logic should trust schema-required fields.
18+
19+
## Server Shape
20+
21+
Each `mcpServers` entry has:
22+
23+
- `id` (server id)
24+
- `enabled` (boolean)
25+
- optional `conditions` (generic key/value matches against global chezmoi data)
26+
- `targets.cursor.enabled`
27+
- `targets.opencode.enabled`
28+
- one of:
29+
- `local` with required `command` and `args`, optional `env`, optional `appendDataArgs`
30+
- `remote` with required `url`, optional `transport`, optional `headers`
31+
32+
## Conditions
33+
34+
`conditions` are evaluated against the root template data object. A server is included only when all condition key/value pairs match.
35+
36+
Example:
37+
38+
```yaml
39+
conditions:
40+
private: false
41+
```
42+
43+
This means the server only renders when `.private` is `false` in chezmoi data.
44+
45+
## appendDataArgs
46+
47+
For local servers, `appendDataArgs` appends values from global template data to the command args list.
48+
49+
Example:
50+
51+
```yaml
52+
local:
53+
command: "mise"
54+
args: ["x", "node@22", "--", "npx", "-y", "@mauricio.wolff/mcp-obsidian@0.8.2"]
55+
appendDataArgs:
56+
- "obsidianVaultPath"
57+
```
58+
59+
If `obsidianVaultPath` exists in template data, its value is appended to `args`.
60+
61+
## Validation
62+
63+
Schema:
64+
65+
- `schemas/mcp-servers.schema.json`
66+
67+
Recommended checks after changes:
68+
69+
1. Render both templates with `chezmoi execute-template`.
70+
2. Validate rendered Cursor MCP JSON.
71+
3. Run `pre-commit run --all-files` (on Windows, run via WSL in this repo).

home/.chezmoi.toml.tmpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{{- $codespaces:= env "CODESPACES" | not | not -}}
22
{{- $private := or (eq .chezmoi.os "windows") (stat (joinPath .chezmoi.homeDir ".private") | not | not) -}}
3+
{{- $obsidianVaultPath := joinPath .chezmoi.homeDir "Documents" "Obsidian" "Vault" -}}
34
[data]
45
codespaces = {{ $codespaces }}
56
private = {{ $private }}
7+
obsidianVaultPath = {{ $obsidianVaultPath | quote }}
68

79
[bitwarden]
810
command = "bw"

home/.chezmoidata/mcp-servers.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mcpServers:
33
- id: atlassian
44
enabled: true
55
conditions:
6-
requirePrivateFalse: true
6+
private: false
77
targets:
88
opencode:
99
enabled: true
@@ -15,7 +15,7 @@ mcpServers:
1515
- id: mcp-atlassian
1616
enabled: true
1717
conditions:
18-
requirePrivateFalse: true
18+
private: false
1919
targets:
2020
opencode:
2121
enabled: true
@@ -44,7 +44,8 @@ mcpServers:
4444
- "npx"
4545
- "-y"
4646
- "@mauricio.wolff/mcp-obsidian@0.8.2"
47-
appendVaultPath: true
47+
appendDataArgs:
48+
- "obsidianVaultPath"
4849
env: {}
4950

5051
- id: chrome-devtools

home/.chezmoitemplates/cursor-settings.json.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"workbench.iconTheme": "catppuccin-mocha",
77
"cursor.composer.usageSummaryDisplay": "always",
88
"cursor.general.enableShadowWorkspace": true,
9-
"obsidianPreview.vaultPath": "{{ .chezmoi.homeDir }}/Documents/Obsidian/Vault",
9+
"obsidianPreview.vaultPath": "{{ if hasKey . `obsidianVaultPath` }}{{ index . `obsidianVaultPath` }}{{ else }}{{ .chezmoi.homeDir }}/Documents/Obsidian/Vault{{ end }}",
1010
"update.releaseTrack": "prerelease",
1111
"terminal.integrated.fontFamily": "FiraCode Nerd Font Mono",
1212
"terminal.integrated.defaultProfile.linux": "zsh",

home/dot_config/opencode/opencode.jsonc.tmpl

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -153,46 +153,33 @@
153153

154154
"mcp": {
155155
{{- $servers := index . "mcpServers" -}}
156-
{{- $isPrivate := and (hasKey . "private") .private -}}
157156
{{- $first := true -}}
158157
{{- range $server := $servers -}}
159-
{{- $isPrivateBlocked := and (hasKey $server "conditions") (index $server.conditions "requirePrivateFalse") $isPrivate -}}
160-
{{- if and $server.targets.opencode.enabled (not $isPrivateBlocked) -}}
158+
{{- $conditionsMatch := true -}}
159+
{{- if hasKey $server "conditions" -}}
160+
{{- range $conditionKey, $expected := $server.conditions -}}
161+
{{- if and (hasKey $ $conditionKey) (eq (index $ $conditionKey) $expected) -}}
162+
{{- else -}}
163+
{{- $conditionsMatch = false -}}
164+
{{- end -}}
165+
{{- end -}}
166+
{{- end -}}
167+
{{- if and $server.targets.opencode.enabled $conditionsMatch -}}
161168
{{- if $first }}{{- $first = false }}{{ else }},{{ end }}
162169
"{{ $server.id }}": {
163-
{{- if hasKey $server.targets.opencode "local" }}
170+
{{- if hasKey $server "local" }}
164171
{{- $openCodeCommandName := $server.local.command -}}
165-
{{- if hasKey $server.targets.opencode.local "command" -}}
166-
{{- $openCodeCommandName = $server.targets.opencode.local.command -}}
167-
{{- end -}}
168172
{{- $openCodeCommandArgs := $server.local.args -}}
169-
{{- if hasKey $server.targets.opencode.local "args" -}}
170-
{{- $openCodeCommandArgs = $server.targets.opencode.local.args -}}
171-
{{- end -}}
172173
{{- $openCodeCommand := prepend $openCodeCommandArgs $openCodeCommandName -}}
173-
{{- if hasKey $server.targets.opencode.local "appendVaultPath" -}}
174-
{{- if $server.targets.opencode.local.appendVaultPath -}}
175-
{{- $openCodeCommand = append $openCodeCommand (printf "%s/Documents/Obsidian/Vault" $.chezmoi.homeDir) -}}
174+
{{- if hasKey $server.local "appendDataArgs" -}}
175+
{{- range $dataKey := $server.local.appendDataArgs -}}
176+
{{- if hasKey $ $dataKey -}}
177+
{{- $openCodeCommand = append $openCodeCommand (index $ $dataKey) -}}
176178
{{- end -}}
177-
{{- else if and (hasKey $server.local "appendVaultPath") $server.local.appendVaultPath -}}
178-
{{- $openCodeCommand = append $openCodeCommand (printf "%s/Documents/Obsidian/Vault" $.chezmoi.homeDir) -}}
179179
{{- end -}}
180-
"type": "local",
181-
"command": {{ $openCodeCommand | toPrettyJson " " }},
182-
{{- else if hasKey $server "local" }}
183-
{{- $openCodeCommand := prepend $server.local.args $server.local.command -}}
184-
{{- if and (hasKey $server.local "appendVaultPath") $server.local.appendVaultPath -}}
185-
{{- $openCodeCommand = append $openCodeCommand (printf "%s/Documents/Obsidian/Vault" $.chezmoi.homeDir) -}}
186180
{{- end -}}
187181
"type": "local",
188182
"command": {{ $openCodeCommand | toPrettyJson " " }},
189-
{{- else if hasKey $server.targets.opencode "remote" }}
190-
{{- $openCodeUrl := $server.remote.url -}}
191-
{{- if hasKey $server.targets.opencode.remote "url" -}}
192-
{{- $openCodeUrl = $server.targets.opencode.remote.url -}}
193-
{{- end -}}
194-
"type": "remote",
195-
"url": "{{ $openCodeUrl }}",
196183
{{- else if hasKey $server "remote" }}
197184
"type": "remote",
198185
"url": "{{ $server.remote.url }}",

home/dot_cursor/mcp.json.tmpl

Lines changed: 15 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,35 @@
11
{
22
"mcpServers": {
33
{{- $servers := index . "mcpServers" -}}
4-
{{- $isPrivate := and (hasKey . "private") .private -}}
54
{{- $first := true -}}
65
{{- range $server := $servers -}}
7-
{{- $isPrivateBlocked := and (hasKey $server "conditions") (index $server.conditions "requirePrivateFalse") $isPrivate -}}
8-
{{- if and $server.targets.cursor.enabled (not $isPrivateBlocked) -}}
9-
{{- if hasKey $server.targets.cursor "local" -}}
10-
{{- $cursorCommand := $server.local.command -}}
11-
{{- if hasKey $server.targets.cursor.local "command" -}}
12-
{{- $cursorCommand = $server.targets.cursor.local.command -}}
6+
{{- $conditionsMatch := true -}}
7+
{{- if hasKey $server "conditions" -}}
8+
{{- range $conditionKey, $expected := $server.conditions -}}
9+
{{- if and (hasKey $ $conditionKey) (eq (index $ $conditionKey) $expected) -}}
10+
{{- else -}}
11+
{{- $conditionsMatch = false -}}
12+
{{- end -}}
1313
{{- end -}}
14-
{{- $cursorArgs := $server.local.args -}}
15-
{{- if hasKey $server.targets.cursor.local "args" -}}
16-
{{- $cursorArgs = $server.targets.cursor.local.args -}}
1714
{{- end -}}
15+
{{- if and $server.targets.cursor.enabled $conditionsMatch -}}
16+
{{- if hasKey $server "local" -}}
17+
{{- $cursorCommand := $server.local.command -}}
18+
{{- $cursorArgs := $server.local.args -}}
1819
{{- $cursorEnv := (default (dict) $server.local.env) -}}
19-
{{- if hasKey $server.targets.cursor.local "env" -}}
20-
{{- $cursorEnv = $server.targets.cursor.local.env -}}
20+
{{- if hasKey $server.local "appendDataArgs" -}}
21+
{{- range $dataKey := $server.local.appendDataArgs -}}
22+
{{- if hasKey $ $dataKey -}}
23+
{{- $cursorArgs = append $cursorArgs (index $ $dataKey) -}}
2124
{{- end -}}
22-
{{- if hasKey $server.targets.cursor.local "appendVaultPath" -}}
23-
{{- if $server.targets.cursor.local.appendVaultPath -}}
24-
{{- $cursorArgs = append $cursorArgs (printf "%s/Documents/Obsidian/Vault" $.chezmoi.homeDir) -}}
2525
{{- end -}}
26-
{{- else if and (hasKey $server.local "appendVaultPath") $server.local.appendVaultPath -}}
27-
{{- $cursorArgs = append $cursorArgs (printf "%s/Documents/Obsidian/Vault" $.chezmoi.homeDir) -}}
2826
{{- end -}}
2927
{{- if $first }}{{- $first = false }}{{ else }},{{ end }}
3028
"{{ $server.id }}": {
3129
"command": "{{ $cursorCommand }}",
3230
"env": {{ $cursorEnv | toPrettyJson " " }},
3331
"args": {{ $cursorArgs | toPrettyJson " " }}
3432
}
35-
{{- else if hasKey $server "local" -}}
36-
{{- $cursorCommand := $server.local.command -}}
37-
{{- $cursorArgs := $server.local.args -}}
38-
{{- if and (hasKey $server.local "appendVaultPath") $server.local.appendVaultPath -}}
39-
{{- $cursorArgs = append $cursorArgs (printf "%s/Documents/Obsidian/Vault" $.chezmoi.homeDir) -}}
40-
{{- end -}}
41-
{{- if $first }}{{- $first = false }}{{ else }},{{ end }}
42-
"{{ $server.id }}": {
43-
"command": "{{ $cursorCommand }}",
44-
"env": {{ (default (dict) $server.local.env) | toPrettyJson " " }},
45-
"args": {{ $cursorArgs | toPrettyJson " " }}
46-
}
47-
{{- else if hasKey $server.targets.cursor "remote" -}}
48-
{{- $cursorTransport := "streamableHttp" -}}
49-
{{- if hasKey $server.targets.cursor.remote "transport" -}}
50-
{{- $cursorTransport = $server.targets.cursor.remote.transport -}}
51-
{{- end -}}
52-
{{- $cursorUrl := $server.remote.url -}}
53-
{{- if hasKey $server.targets.cursor.remote "url" -}}
54-
{{- $cursorUrl = $server.targets.cursor.remote.url -}}
55-
{{- end -}}
56-
{{- $cursorHeaders := dict -}}
57-
{{- if hasKey $server.targets.cursor.remote "headers" -}}
58-
{{- $cursorHeaders = $server.targets.cursor.remote.headers -}}
59-
{{- end -}}
60-
{{- if $first }}{{- $first = false }}{{ else }},{{ end }}
61-
"{{ $server.id }}": {
62-
"transport": "{{ $cursorTransport }}",
63-
"url": "{{ $cursorUrl }}",
64-
"headers": {{ $cursorHeaders | toPrettyJson " " }}
65-
}
6633
{{- else if hasKey $server "remote" -}}
6734
{{- $cursorTransport := "streamableHttp" -}}
6835
{{- if hasKey $server.remote "transport" -}}

0 commit comments

Comments
 (0)