Skip to content

Commit 45e772d

Browse files
committed
feat(mcp): add Server Registry tools + port registry in servers.md (v0.10.0)
- Add MCP tools: servers_check_port, servers_list_registry, servers_add_entry - servers_check_port: returns free/allocated/mutex status for a port - servers_list_registry: returns full Port Allocation Registry as structured JSON - servers_add_entry: validates and writes new rows to tools/servers.md, rejects mutex ports (80/443) and duplicate allocations - Add tools/mcp-server/src/handlers/servers.ts with registry parser and appendRegistryRow writer - Bump MCP server to v0.10.0 (26 tools across 7 areas) - Add Port Allocation Registry table to tools/servers.md with all known services and agent-readable 6-step workflow callout at top - Add Server Registry Workflow section and trigger to AGENTS.md - Fix tools/servers-audit.sh: remove 'local' keyword at top-level scope (lines 1225, 1229, 1250); change grep -Eq to grep -Fxq for process names with regex metacharacters (GitHub Desktop Helper (Renderer)) - Fix broken symlink ~/bin/servers-audit.sh -> tools/servers-audit.sh - Add tools/servers-audit-bugs.md documenting the three bugs - Update CHANGELOG.md [2.0.0] with full change summary
1 parent 62775de commit 45e772d

8 files changed

Lines changed: 498 additions & 9 deletions

File tree

AGENTS.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ If you need a **persistent background server** (e.g., for HTTP mode or external
129129

130130
#### Available MCP Tools
131131

132-
The server exposes **23 typed tools** across 6 areas:
132+
The server exposes **26 typed tools** across 7 areas:
133133

134134
| Area | Tools |
135135
|------|-------|
@@ -139,6 +139,7 @@ The server exposes **23 typed tools** across 6 areas:
139139
| **Query Monitor** | `qm_profile_page`, `qm_slow_queries`, `qm_duplicate_queries` |
140140
| **AJAX Testing** | `wp_ajax_test` |
141141
| **Tmux** | `tmux_start`, `tmux_send`, `tmux_capture`, `tmux_stop`, `tmux_list`, `tmux_status` |
142+
| **Server Registry** | `servers_check_port`, `servers_list_registry`, `servers_add_entry` |
142143

143144
#### Available MCP Prompts
144145

@@ -190,8 +191,25 @@ For detailed command syntax, parameters, examples, and troubleshooting, see:
190191
| “login to WP admin”, “browser automation”, “inspect DOM” | `pw-auth` |
191192
| “test this AJAX endpoint” | `wp-ajax-test` |
192193
| “slow”, “bottleneck”, “profile” | QM profiling (`qm_profile_page`, `qm_slow_queries`, `qm_duplicate_queries`) + WP Performance Timer |
193-
| “fix”, “verify”, “iterate”, “debug” | Fix-Iterate Loop |
194+
| “fix”, “verify”, “iterate”, “debug” | Fix-Iterate Loop || "install", "add", "set up" a new server, tool, or daemon | **Server Registry Workflow** (see below) |
194195

196+
### Server Registry Workflow
197+
198+
**Any time a new networked service is installed** (Homebrew daemon, Docker container, local dev server, etc.), run this workflow before and after:
199+
200+
1. **Read the port registry**`cat ~/bin/ai-ddtk/tools/servers.md` — check for conflicts with the intended port
201+
2. **Reserve the port** — add a row to the Port Allocation Registry in `tools/servers.md` before installing
202+
3. **Install and start** the service
203+
4. **Run the audit**`~/bin/ai-ddtk/tools/servers-audit.sh --output /tmp/servers-now.md`
204+
5. **Confirm no new conflicts** in the audit output
205+
6. **Update the registry entry** with confirmed port, hostname, and any notes
206+
207+
**Port 80/443 are a mutex.** Only one service holds them at a time: Dify (Docker), Valet (*.test), or Local WP (*.local). Never bind `0.0.0.0:80` from a new service without checking first.
208+
209+
**Hostname namespace rules:**
210+
- `*.local` — managed exclusively by Local WP; never add manually
211+
- `*.test` — managed by Valet/dnsmasq; add via `valet proxy <name> --to=http://127.0.0.1:<port>`
212+
- Manual `/etc/hosts` entries for `.test` hostnames **override dnsmasq** — avoid unless intentional
195213
### Task Management
196214

197215
Use task tools for complex or multi-step work. Mark tasks complete immediately after finishing them.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1313
- Do not edit a version block that has already been committed and pushed
1414
-->
1515

16+
## [2.0.0] - 2026-04-11
17+
18+
### Added
19+
- **MCP tools: `servers_check_port`, `servers_list_registry`, `servers_add_entry`** — three new Server Registry tools in MCP server v0.10.0. `servers_list_registry` returns the full Port Allocation Registry from `tools/servers.md` as structured JSON so agents never need to parse markdown. `servers_check_port` returns `free`, `allocated`, or `mutex` for any given port with the conflicting entry if taken. `servers_add_entry` validates the port is not already allocated or a mutex port (80/443), then writes the new row to `tools/servers.md` with correct table formatting — preventing duplicate ports and malformed table edits. These tools enforce the Server Registry Workflow automatically rather than relying on agents reading instructions.
20+
- **Server Registry Workflow in `AGENTS.md`** — new "Server Registry Workflow" section and workflow trigger (`"install", "add", "set up" a new server, tool, or daemon`) instructs all agents to read `tools/servers.md`, reserve a port, install, audit, and update before and after adding any networked service.
21+
- **Port Allocation Registry in `tools/servers.md`** — machine-readable registry table with all known local services (Dify, Valet, Local WP, Homebrew MySQL/Postgres/Ollama, Docker services) and port 80/443 mutex rules. Agents are instructed at the top of the file with a 6-step workflow callout.
22+
- **Bug fixes: `tools/servers-audit.sh`** — three crashes fixed: (1) `local` keyword used outside a function at lines 1225, 1229, 1250 — replaced with bare variable initialization; (2) `grep -Eq "^${lower_name}$"` with unescaped regex metacharacters from process names containing `(` — changed to `grep -Fxq` (fixed-string whole-line match); (3) broken symlink `~/bin/servers-audit.sh` pointing to non-existent `experimental/servers-audit.sh` — updated to `~/bin/ai-ddtk/tools/servers-audit.sh`.
23+
24+
### Changed
25+
- **MCP server version bump to v0.10.0** — new Server Registry area added; tool count updated from 23 to 26 across 7 areas in AGENTS.md.
26+
1627
## [1.9.1] - 2026-04-09
1728

1829
### Changed

tools/mcp-server/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ai-ddtk-mcp",
3-
"version": "0.9.0",
4-
"description": "AI-DDTK MCP server for LocalWP, pw-auth, wp-ajax-test, tmux, WPCC, Query Monitor, and post-flight session cleanup workflows",
3+
"version": "0.10.0",
4+
"description": "AI-DDTK MCP server for LocalWP, pw-auth, wp-ajax-test, tmux, WPCC, Query Monitor, post-flight session cleanup, and server port registry workflows",
55
"type": "module",
66
"main": "dist/src/index.js",
77
"bin": {
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { readFile, writeFile } from "node:fs/promises";
2+
import path from "node:path";
3+
import { homedir } from "node:os";
4+
5+
const REGISTRY_HEADER = "| Port | Service | Owner | Hostname | Notes |";
6+
const REGISTRY_SEPARATOR = "|------|---------|-------|----------|-------|";
7+
const MUTEX_PORTS = new Set([80, 443]);
8+
9+
export type PortStatus = "free" | "allocated" | "mutex";
10+
11+
export type RegistryEntry = {
12+
port: number;
13+
service: string;
14+
owner: string;
15+
hostname: string;
16+
notes: string;
17+
};
18+
19+
export type ServersCheckPortResult = Record<string, unknown> & {
20+
port: number;
21+
status: PortStatus;
22+
entry: RegistryEntry | null;
23+
registryPath: string;
24+
};
25+
26+
export type ServersListRegistryResult = Record<string, unknown> & {
27+
entries: RegistryEntry[];
28+
allocatedPorts: number[];
29+
mutexPorts: number[];
30+
registryPath: string;
31+
};
32+
33+
export type ServersAddEntryResult = Record<string, unknown> & {
34+
added: boolean;
35+
entry: RegistryEntry;
36+
conflict: RegistryEntry | null;
37+
registryPath: string;
38+
message: string;
39+
};
40+
41+
export interface ServersHandlerDeps {
42+
repoRoot: string;
43+
registryPath?: string;
44+
}
45+
46+
function resolveRegistryPath(repoRoot: string, override?: string): string {
47+
if (override) {
48+
return override.startsWith("~") ? override.replace("~", homedir()) : override;
49+
}
50+
return path.join(repoRoot, "tools/servers.md");
51+
}
52+
53+
/**
54+
* Parse the Port Allocation Registry table from servers.md.
55+
* Returns only rows that begin with a numeric port (skips mutex-only rows
56+
* that start with bold text like "**MUTEX**").
57+
*/
58+
function parseRegistry(content: string): RegistryEntry[] {
59+
const lines = content.split("\n");
60+
const tableStart = lines.findIndex((l) => l.includes(REGISTRY_HEADER));
61+
if (tableStart === -1) return [];
62+
63+
const entries: RegistryEntry[] = [];
64+
65+
for (let i = tableStart + 2; i < lines.length; i++) {
66+
const line = lines[i].trim();
67+
// Stop at blank line or non-table line
68+
if (!line.startsWith("|") || line === "") break;
69+
70+
const cells = line
71+
.split("|")
72+
.map((c) => c.trim())
73+
.filter((_, idx, arr) => idx > 0 && idx < arr.length - 1);
74+
75+
if (cells.length < 4) continue;
76+
77+
const portNum = parseInt(cells[0] ?? "", 10);
78+
if (isNaN(portNum)) continue; // skip mutex header rows
79+
80+
entries.push({
81+
port: portNum,
82+
service: cells[1] ?? "",
83+
owner: cells[2] ?? "",
84+
hostname: cells[3] ?? "",
85+
notes: cells[4] ?? "",
86+
});
87+
}
88+
89+
return entries;
90+
}
91+
92+
/**
93+
* Append a new row immediately before the blank line that follows the last
94+
* table row, preserving the existing table formatting.
95+
*/
96+
function appendRegistryRow(content: string, entry: RegistryEntry): string {
97+
const lines = content.split("\n");
98+
const tableStart = lines.findIndex((l) => l.includes(REGISTRY_HEADER));
99+
if (tableStart === -1) {
100+
throw new Error("Port Allocation Registry table not found in servers.md");
101+
}
102+
103+
// Find the last table row (last line beginning with "|" after tableStart)
104+
let lastTableRow = tableStart + 1; // separator line
105+
for (let i = tableStart + 2; i < lines.length; i++) {
106+
if (lines[i].trim().startsWith("|")) {
107+
lastTableRow = i;
108+
} else {
109+
break;
110+
}
111+
}
112+
113+
const newRow = `| ${entry.port} | ${entry.service} | ${entry.owner} | ${entry.hostname} | ${entry.notes} |`;
114+
lines.splice(lastTableRow + 1, 0, newRow);
115+
return lines.join("\n");
116+
}
117+
118+
export function createServersHandlers(deps: ServersHandlerDeps) {
119+
const { repoRoot, registryPath: registryPathOverride } = deps;
120+
121+
async function checkPort(port: number): Promise<ServersCheckPortResult> {
122+
const registryPath = resolveRegistryPath(repoRoot, registryPathOverride);
123+
const content = await readFile(registryPath, "utf8");
124+
const entries = parseRegistry(content);
125+
126+
if (MUTEX_PORTS.has(port)) {
127+
return {
128+
port,
129+
status: "mutex",
130+
entry: null,
131+
registryPath,
132+
};
133+
}
134+
135+
const match = entries.find((e) => e.port === port);
136+
return {
137+
port,
138+
status: match ? "allocated" : "free",
139+
entry: match ?? null,
140+
registryPath,
141+
};
142+
}
143+
144+
async function listRegistry(): Promise<ServersListRegistryResult> {
145+
const registryPath = resolveRegistryPath(repoRoot, registryPathOverride);
146+
const content = await readFile(registryPath, "utf8");
147+
const entries = parseRegistry(content);
148+
149+
return {
150+
entries,
151+
allocatedPorts: entries.map((e) => e.port),
152+
mutexPorts: [...MUTEX_PORTS],
153+
registryPath,
154+
};
155+
}
156+
157+
async function addEntry(entry: RegistryEntry): Promise<ServersAddEntryResult> {
158+
const registryPath = resolveRegistryPath(repoRoot, registryPathOverride);
159+
const content = await readFile(registryPath, "utf8");
160+
const entries = parseRegistry(content);
161+
162+
// Check for conflict
163+
const conflict = entries.find((e) => e.port === entry.port) ?? null;
164+
if (conflict) {
165+
return {
166+
added: false,
167+
entry,
168+
conflict,
169+
registryPath,
170+
message: `Port ${entry.port} is already allocated to "${conflict.service}" (${conflict.owner}). Choose a different port.`,
171+
};
172+
}
173+
174+
if (MUTEX_PORTS.has(entry.port)) {
175+
return {
176+
added: false,
177+
entry,
178+
conflict: null,
179+
registryPath,
180+
message: `Port ${entry.port} is a mutex port shared by Dify, Valet, and Local WP. Do not register individual services on it.`,
181+
};
182+
}
183+
184+
const updated = appendRegistryRow(content, entry);
185+
await writeFile(registryPath, updated, "utf8");
186+
187+
return {
188+
added: true,
189+
entry,
190+
conflict: null,
191+
registryPath,
192+
message: `Port ${entry.port} registered for "${entry.service}" in ${registryPath}.`,
193+
};
194+
}
195+
196+
return { checkPort, listRegistry, addEntry };
197+
}

0 commit comments

Comments
 (0)