Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ All notable changes to this project will be documented in this file, per [the Ke

### Added

- Global MCP endpoint at `/sites/mcp`, not bound to any site — configure it once (e.g. in `~/.claude.json`) and manage Local from anywhere, instead of needing an enabled site's folder open first. It serves the Local-wide tools (`list_sites`, `create_site`, `list_service_versions`), the site lifecycle tools, and the new Agent Tools management tools. Site-scoped tools stay on `/sites/{siteId}/mcp` and return an error pointing there if called globally.
- `enable_agent_tools` and `disable_agent_tools` MCP tools — turn Agent Tools on or off for an existing site over MCP, the same as clicking Enable in Local's UI. `enable_agent_tools` returns the site's own MCP endpoint URL, so an agent can bootstrap from the global endpoint to a site-scoped one.
- `agent_tools_status` MCP tool — report which sites have Agent Tools enabled, which agents are configured, the project directory, whether the site is registered with the MCP server, and each site's MCP endpoint URL.
- `create_site` MCP tool — create a new WordPress site in Local, with optional PHP / database / web server versions, multisite mode, WordPress admin credentials, and Xdebug. Returns as soon as the site is registered so the call does not outlive the MCP client's request timeout; poll `site_status` until the site reports `running`, or pass `wait: true` to block (props [@ivanlopez](https://github.com/ivanlopez) via [#80](https://github.com/10up/localwp-agent-tools/pull/80)).
- `create_site` can enable Agent Tools on the site it creates via `enableAgentTools`, registering it with the MCP server and writing its MCP config and context files for the agents named in `agents` (props [@ivanlopez](https://github.com/ivanlopez) via [#80](https://github.com/10up/localwp-agent-tools/pull/80)).
- `list_service_versions` MCP tool — list the PHP, database, and web server versions available to `create_site`, flagging which are already installed versus downloaded on demand (props [@ivanlopez](https://github.com/ivanlopez) via [#80](https://github.com/10up/localwp-agent-tools/pull/80)).
Expand Down
50 changes: 44 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,50 @@ Then open the site folder in your AI tool of choice and you're ready to go.

## Architecture

The MCP server runs as a single HTTP server inside Local's Electron main process — no separate Node.js processes per site. Each site gets its own endpoint:
The MCP server runs as a single HTTP server inside Local's Electron main process — no separate Node.js processes per site. It serves two kinds of endpoint:

```
http://localhost:{port}/sites/{siteId}/mcp
http://localhost:{port}/sites/mcp # global — all of Local
http://localhost:{port}/sites/{siteId}/mcp # one specific site
```

The server uses the MCP Streamable HTTP transport. The port is stable across restarts (persisted at `~/.local-agent-tools/port`, default 24842).

Sites remain registered even when stopped, so the MCP endpoint is always reachable. Tools that need running services (WP-CLI, database) return appropriate errors; file-based tools (config, logs, site info) work regardless. Config is refreshed on each tool call, so starting a site automatically makes database tools work without reconnecting.

### The global endpoint

`/sites/mcp` is not tied to any site, so you configure it once and use it from anywhere — no need to open a particular site folder first. It serves the tools that address Local itself (`list_sites`, `create_site`, `list_service_versions`), the site lifecycle tools (`site_start` and friends, which take an explicit `siteId`), and the tools that turn Agent Tools on and off per site (`enable_agent_tools`, `disable_agent_tools`, `agent_tools_status`).

That makes it the way to bootstrap: connect to the global endpoint, create or find a site, enable Agent Tools on it, and `enable_agent_tools` hands back that site's own endpoint URL for the site-scoped work.

The site-scoped tools (`wp_cli`, the log readers, the wp-config tools, `get_site_info`, `site_health_check`) are deliberately not served here — they need a bound site, and calling one returns an error pointing at the per-site endpoint instead.

To add it to Claude Code, using the persisted port:

```bash
claude mcp add --scope user --transport http local-wp-global \
"http://localhost:$(cat ~/.local-agent-tools/port)/sites/mcp"
```

`--scope user` is the part that makes it global. Without it `claude mcp add` defaults to `--scope local`, which registers the server only for the directory you ran it in — the opposite of the point here. Use `--scope project` instead if you want it committed to a repo's `.mcp.json` for the team.

Or by hand, as a top-level `mcpServers` entry in `~/.claude.json` (Cursor, Windsurf, and VS Code use the same shapes as the per-site config the add-on writes):

```json
{
"mcpServers": {
"local-wp-global": { "type": "http", "url": "http://localhost:24842/sites/mcp" }
}
}
```

`claude mcp list` confirms it connected.

`curl http://localhost:{port}/health` lists the port's registered sites and confirms the global endpoint is up.

One caveat: the global endpoint is reachable by any process on the machine, as the per-site endpoints already are — the server binds `127.0.0.1` and has no authentication. It widens what that means in practice, since site management and `create_site` are now reachable from one well-known URL rather than only from an enabled site's.

## Supported Agents

| Agent | MCP Config | Context File |
Expand All @@ -36,7 +70,7 @@ Sites remain registered even when stopped, so the MCP endpoint is always reachab
| Windsurf | `.windsurf/mcp.json` | `.windsurfrules` |
| VS Code Copilot | `.vscode/mcp.json` | `.github/copilot-instructions.md` |

## MCP Tools (14 total)
## MCP Tools (17 total)

| Category | Tools | Description |
| --------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------- |
Expand All @@ -55,6 +89,9 @@ Sites remain registered even when stopped, so the MCP endpoint is always reachab
| | `list_sites` | List all Local sites with status |
| | `create_site` | Create a new WordPress site in Local, optionally enabling Agent Tools on it |
| | `list_service_versions` | PHP, database, and web server versions available to `create_site` |
| **Agent Tools** | `enable_agent_tools` | Enable Agent Tools on a site — register it and write its MCP config and context files |
| | `disable_agent_tools` | Disable Agent Tools on a site and remove what it wrote |
| | `agent_tools_status` | Report which sites have Agent Tools enabled, their agents, and their MCP endpoint URLs |

### Creating sites

Expand Down Expand Up @@ -148,19 +185,20 @@ agent-tools/
├── src/ # Add-on source (TypeScript)
│ ├── main.ts # Main process — lifecycle hooks, IPC, MCP server startup
│ ├── renderer.tsx # Renderer process — React UI
│ ├── mcp-server.ts # HTTP MCP server — session management, Streamable HTTP transport
│ ├── mcp-server.ts # HTTP MCP server — routing, session management, Streamable HTTP transport
│ ├── helpers/
│ │ ├── site-config.ts # SiteConfig type and SiteConfigRegistry
│ │ ├── paths.ts # Platform-specific binary resolution (PHP, MySQL, WP-CLI)
│ │ ├── new-site.ts # Pure helpers for create_site: nicename, domain, and path validation
│ │ └── port.ts # Stable port allocation with file persistence
│ └── tools/ # MCP tool implementations
│ ├── index.ts # Aggregates definitions, routes handleToolCall()
│ ├── index.ts # Aggregates definitions (full vs global), routes handleToolCall()
│ ├── wpcli.ts # wp_cli
│ ├── logs.ts # read_error_log, read_access_log, wp_debug_toggle
│ ├── config.ts # read_wp_config, edit_wp_config
│ ├── site.ts # get_site_info, site_health_check
│ └── environment.ts # site_start, site_stop, site_restart, site_status, list_sites, create_site, list_service_versions
│ ├── environment.ts # site_start, site_stop, site_restart, site_status, list_sites, create_site, list_service_versions
│ └── agent-tools.ts # enable_agent_tools, disable_agent_tools, agent_tools_status
├── lib/ # Compiled output
├── package.json
└── tsconfig.json
Expand Down
138 changes: 132 additions & 6 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,24 @@
findWpCli,
} from './helpers/paths';
import { SiteConfig, SiteConfigRegistry } from './helpers/site-config';
import { findAvailablePort, savePort, removePortFile, removePortFileSync } from './helpers/port';

Check warning on line 14 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

'removePortFile' is defined but never used
import { createMcpHttpServer, startMcpHttpServer, stopMcpHttpServer, closeSessionsForSite } from './mcp-server';
import { LocalApi, CreateSiteOptions, CreateSiteResult, ServiceVersion, ServiceVersions } from './tools';
import {
createMcpHttpServer,
startMcpHttpServer,
stopMcpHttpServer,
closeSessionsForSite,
GLOBAL_MCP_PATH,
} from './mcp-server';
import {
LocalApi,
CreateSiteOptions,
CreateSiteResult,
ServiceVersion,
ServiceVersions,
AgentToolsSiteStatus,
EnableAgentToolsOptions,
AgentName,
} from './tools';
import {
BUILT_IN_SITE_DEFAULTS,
NewSiteDefaults,
Expand Down Expand Up @@ -193,8 +208,12 @@
* Builds the MCP server entry for a specific agent.
* Each agent has different JSON shapes for HTTP MCP servers.
*/
function buildSiteMcpUrl(port: number, siteId: string): string {
return `http://localhost:${port}/sites/${siteId}/mcp`;
}

function buildMcpServerEntry(agent: AgentTarget, port: number, siteId: string): Record<string, any> {

Check warning on line 215 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const url = `http://localhost:${port}/sites/${siteId}/mcp`;
const url = buildSiteMcpUrl(port, siteId);

switch (agent) {
case 'claude':
Expand All @@ -215,10 +234,10 @@
*/
async function mergeMcpConfig(
configPath: string,
serverEntry: Record<string, any>,

Check warning on line 237 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
topLevelKey: string,
): Promise<void> {
let existing: any = {};

Check warning on line 240 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

if (await fs.pathExists(configPath)) {
try {
Expand Down Expand Up @@ -409,7 +428,7 @@
// Core Functions
// ---------------------------------------------------------------------------

async function setupSite(site: Local.Site, notifier: any, projectDir: string, agents: AgentTarget[]): Promise<void> {

Check warning on line 431 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const sitePath = getSitePath(site);
const projectPath = getProjectPath(sitePath, projectDir);

Expand Down Expand Up @@ -461,7 +480,7 @@
});
}

async function teardownSite(site: Local.Site, notifier: any): Promise<void> {

Check warning on line 483 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const sitePath = getSitePath(site);
const projectDir = getStoredProjectDir(site);
const projectPath = getProjectPath(sitePath, projectDir);
Expand Down Expand Up @@ -500,7 +519,7 @@
});
}

async function changeProjectDir(site: Local.Site, newProjectDir: string, notifier: any): Promise<void> {

Check warning on line 522 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const sitePath = getSitePath(site);
const oldProjectDir = getStoredProjectDir(site);
const oldPath = getProjectPath(sitePath, oldProjectDir);
Expand Down Expand Up @@ -549,7 +568,7 @@
});
}

async function updateAgents(site: Local.Site, newAgents: AgentTarget[], notifier: any): Promise<void> {

Check warning on line 571 in src/main.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
if (!isAgentToolsEnabled(site)) return;

const sitePath = getSitePath(site);
Expand Down Expand Up @@ -603,6 +622,44 @@
});
}

/**
* Enable Agent Tools on a site, or re-apply it to one that is already enabled.
*
* setupSite() only ever adds files, so it cannot be reused verbatim for a site
* that is already enabled: moving the project dir would strand config at the old
* location, and narrowing the agent list would strand the dropped agents' files.
* changeProjectDir() and updateAgents() are the paths that clean up after
* themselves, so route through them and let regenerateConfig() refresh the rest.
*/
async function applyAgentToolsSetup(
site: Local.Site,
notifier: any,
projectDir: string,
agents: AgentTarget[],
): Promise<void> {
if (!isAgentToolsEnabled(site)) {
await setupSite(site, notifier, projectDir, agents);
return;
}

// Move first, using the stored agent set, so the files that move are the ones
// that currently exist. Each step writes through SiteData, so re-read between
// them or the next step would persist stale customOptions.
let current = site;

if (getStoredProjectDir(current) !== projectDir) {
await changeProjectDir(current, projectDir, notifier);
current = LocalMain.SiteData.getSite(site.id) ?? current;
}

await updateAgents(current, agents, notifier);
current = LocalMain.SiteData.getSite(site.id) ?? current;

// Rewrite MCP config and context for the resulting agent set — updateAgents
// only touches the agents that changed.
await regenerateConfig(current);
}

async function regenerateConfig(site: Local.Site): Promise<void> {
if (!isAgentToolsEnabled(site)) return;

Expand Down Expand Up @@ -656,6 +713,33 @@
};
}

/**
* Agent Tools state for one site, as reported over the global MCP endpoint.
* Reads straight from SiteData, so it is accurate for sites that were never enabled.
*/
function describeAgentToolsStatus(site: Local.Site): AgentToolsSiteStatus {
const enabled = isAgentToolsEnabled(site);

return {
id: site.id,
name: site.name,
domain: site.domain || '',
sitePath: getSitePath(site),
projectDir: getStoredProjectDir(site),
enabled,
agents: getStoredAgents(site) as AgentName[],
registered: siteConfigRegistry.has(site.id),
mcpUrl: enabled && mcpServerPort ? buildSiteMcpUrl(mcpServerPort, site.id) : null,
};
}

/** Look up a site by id, with a consistent error for the MCP tools. */
function requireSite(siteId: string): Local.Site {
const site = LocalMain.SiteData.getSite(siteId);
if (!site) throw new Error(`Site not found: ${siteId}. Use list_sites to see available site IDs.`);
return site;
}

// ---------------------------------------------------------------------------
// LocalApi Implementation — wraps Local's SiteProcessManager
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -714,8 +798,10 @@
const siteCreationFailures = new Map<string, string>();

interface LocalApiOptions {
/** Enables Agent Tools on a freshly created site. Wired to setupSite() by the add-on entry point. */
enableAgentTools(site: Local.Site, agents: AgentTarget[]): Promise<void>;
/** Enables Agent Tools on a site. Wired to setupSite() by the add-on entry point. */
enableAgentTools(site: Local.Site, agents: AgentTarget[], projectDir?: string): Promise<void>;
/** Disables Agent Tools on a site. Wired to teardownSite() by the add-on entry point. */
disableAgentTools(site: Local.Site): Promise<void>;
}

function createLocalApi(options: LocalApiOptions): LocalApi {
Expand Down Expand Up @@ -960,6 +1046,40 @@

return describe(site, true);
},

async enableAgentTools({ siteId, agents, projectDir }: EnableAgentToolsOptions) {
const site = requireSite(siteId);
const targets = (agents?.length ? agents : ['claude']) as AgentTarget[];

await options.enableAgentTools(site, targets, projectDir ?? '');

// setupSite writes customOptions through SiteData, so re-read to report
// the state that was actually persisted.
return describeAgentToolsStatus(LocalMain.SiteData.getSite(siteId) ?? site);
},

async disableAgentTools(siteId: string) {
const site = requireSite(siteId);

// Idempotent: teardown on a site that was never enabled would still
// rewrite its .gitignore and fire a misleading notification.
if (!isAgentToolsEnabled(site)) {
return describeAgentToolsStatus(site);
}

await options.disableAgentTools(site);

return describeAgentToolsStatus(LocalMain.SiteData.getSite(siteId) ?? site);
},

async getAgentToolsStatus(siteId?: string) {
if (siteId) {
return [describeAgentToolsStatus(requireSite(siteId))];
}

const sites = LocalMain.SiteData.getSites();
return (Object.values(sites) as Local.Site[]).map(describeAgentToolsStatus);
},
};
}

Expand All @@ -972,7 +1092,8 @@

let httpServer: ReturnType<typeof createMcpHttpServer> | null = null;
const localApi = createLocalApi({
enableAgentTools: (site, agents) => setupSite(site, notifier, '', agents),
enableAgentTools: (site, agents, projectDir) => applyAgentToolsSetup(site, notifier, projectDir ?? '', agents),
disableAgentTools: (site) => teardownSite(site, notifier),
});

// Start the MCP HTTP server
Expand All @@ -999,6 +1120,11 @@

await savePort(mcpServerPort);

console.log(
`[Agent Tools] Global endpoint: http://localhost:${mcpServerPort}${GLOBAL_MCP_PATH} ` +
`(per-site: http://localhost:${mcpServerPort}/sites/{siteId}/mcp)`,
);

// Register configs for all sites with Agent Tools enabled (regardless of running status).
// This ensures the MCP endpoint is always reachable — tools that need the site
// running (WP-CLI, DB) will return appropriate errors; file-based tools still work.
Expand Down
Loading
Loading