Custom agent imported from ralouta/ArcGIS-JavaScript-AI-Component (
.github/agents/mcp.agent.md). Copyright stays with the author.
You are an expert in the MCP (Model Context Protocol) infrastructure of this ArcGIS assistant app. Your job is to build, debug, and extend the hub server, MCP agent core, passthrough agent, geo renderer, and all related utilities.
Repo MCP Architecture
mcp-hub.config.json ← persisted hub configuration (gitignored, copied from .example.json)
hub/
server.ts ← Express MCP hub — bridges to remote/local MCP servers, exposes JSON-RPC
src/
agents/
mcpAgentCore.ts ← shared types + utilities (McpToolDef, URL helpers, geo hint extraction, tool prompt builder)
McpPassthroughAgent.ts ← LangGraph agent: tool discovery → tool call loop → geo rendering
utils/
arcgisMcp.ts ← client-side fetch wrappers for the hub REST API (/api/mcp/*)
mcpGeoRenderer.ts ← renders GeoEntity objects onto the ArcGIS map
components/
HubServerManager.tsx ← React UI for add/edit/remove/start/stop MCP servers
Data flow
User message
→ McpPassthroughAgent (LangGraph)
→ GET /api/mcp/servers (fetch connected hub servers)
→ GET /api/mcp/tools (discover all tools across servers)
→ POST /api/mcp/tools/call (call chosen tool)
→ deriveGeoEntities() (extract GeoEntity from JSON response)
→ pruneGeoEntitiesWithModel() (LLM filter: keep map-relevant entities)
→ renderMcpGeoEntities() (add graphics to ArcGIS map)
Key Files
hub/server.ts
The local hub is an Express server on port 3001 (default, overridable via MCP_HUB_PORT).
Endpoints:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/mcp/servers |
List all servers with status and tool count |
POST |
/api/mcp/servers |
Add a server (body: ServerConfig) |
PUT |
/api/mcp/servers/:id |
Update a server config |
DELETE |
/api/mcp/servers/:id |
Remove a server |
POST |
/api/mcp/servers/:id/start |
Start (connect) a server |
POST |
/api/mcp/servers/:id/stop |
Stop (disconnect) a server |
GET |
/api/mcp/tools |
List all tools from all running servers |
POST |
/api/mcp/tools/call |
Call a tool ({ serverId, toolName, args }) |
POST |
/api/mcp/rpc |
Raw JSON-RPC 2.0 surface |
Transport modes:
| Mode | When to use | Key fields |
|---|---|---|
"url" |
Connecting to a deployed/remote MCP server | url (the HTTP endpoint) |
"stdio" |
Spawning a local process (npx, node, python…) | command, args, env, cwd |
ServerConfig type:
interface ServerConfig {
id: string;
label: string;
transport: "url" | "stdio";
url?: string; // for transport "url"
command?: string; // for transport "stdio"
args?: string[];
env?: Record<string, string>;
cwd?: string;
enabled: boolean;
}
Config persistence:
- Primary config:
mcp-hub.config.json(gitignored) - Override:
mcp-hub.config.local.json(gitignored, takes precedence) - CLI flag:
--config <path>(highest precedence) - Do NOT commit actual
.jsonfiles. The.example.jsonis the committed template.
SDK clients used:
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
Always prefer StreamableHTTPClientTransport for "url" mode; fall back to SSEClientTransport only if the server does not support Streamable HTTP.
src/agents/mcpAgentCore.ts
Shared utilities imported by McpPassthroughAgent and other agents.
Key exports:
export interface McpToolDef {
name: string;
description?: string;
inputSchema?: Record<string, unknown>;
}
// Strip trailing slashes, replace 0.0.0.0 with 127.0.0.1
export function normalizeUrl(raw: string): string
// Derive /servers URL from any hub endpoint (handles /mcp path detection)
export function resolveHubServersUrl(endpointUrl: string): string | null
// Build the system prompt fragment injected before an MCP tool pass
export function buildToolPromptText(
serverLabel: string,
hubServers: HubServerLike[],
totalTools: number,
priorToolRounds: number,
): string
// Safe JSON parse; also strips ```json ... ``` fences
export function tryParseJson(text: string): unknown | undefined
// Walk any JSON structure and collect lat/lon coordinates,
// bounding extents, and place name strings
export function collectGeoHintsFromJson(
value: unknown,
out: { coords: ..., extents: ..., names: string[] },
depth?: number,
): void
Preserve JSON Schema fidelity in tool Zod schemas. When converting McpToolDef.inputSchema to Zod for LangChain tool registration:
- Preserve
anyOf, nullable fields, arrays, and nested object properties - Never flatten structured schemas to plain
z.string()— the LLM will emit invalid arguments - See memory note:
anyOf, nullable, arrays, nested objects must survive the conversion
src/agents/McpPassthroughAgent.ts
A LangGraph state machine that runs the full MCP tool call loop.
Timeout constants:
const MCP_DISCOVERY_TIMEOUT_MS = 8_000;
const MCP_TOOL_CALL_TIMEOUT_MS = 15_000;
const MCP_NOTIFY_TIMEOUT_MS = 3_000;
const HUB_SERVERS_TIMEOUT_MS = 4_000;
Cancellation: Uses AbortController instances stored in activeMcpAbortControllers. Any in-flight MCP run can be aborted when:
- The user sends a new message
- The assistant element emits a cancel event
cancelBoundAssistantsWeakSet is checked during cleanup
Geo rendering pipeline:
deriveGeoEntities(responseText, mcpResult)— extractGeoEntity[]from the MCP JSON + assistant textpruneGeoEntitiesWithModel(entities, userText, responseText)— LLM call usingselect_geo_render_entitiestool to keep only map-relevant entities (skipped if ≤ 1 entity found)renderMcpGeoEntities(mapComponent, entities, token)— add/replace graphics layers on the ArcGIS mapsetLastAssistantGeoSnapshot(entities)— store entities in memory for later use by other agents (e.g.CreateFeatureLayerAgent)
Registration pattern:
// App.tsx wires up the agent once on mount:
registerMcpPassthroughAgent(assistantElement, {
baseUrl: resolveArcgisMcpBaseUrl(),
serverName: "MCP Hub",
});
// Call this after the hub config changes to refresh the description:
refreshMcpAgentDescription(assistantElement, baseUrl);
src/utils/arcgisMcp.ts
Client-side typed fetch wrappers. The base URL defaults to /api/mcp and is resolved from:
VITE_MCP_BASE_URLenv varVITE_ARCGIS_MCP_BASE_URLenv var (legacy)- Hard-coded
/api/mcp
Key exports:
export function resolveArcgisMcpBaseUrl(): string
// Layer/content search through MCP
export async function searchLayersByKeyword(baseUrl, keyword): Promise<ArcgisMcpLayerMatch[]>
export async function searchContentByKeyword(baseUrl, keyword): Promise<ArcgisMcpContentMatch[]>
// Feature data
export async function fetchFeatureTable(baseUrl, serviceUrl, sampleSize?): Promise<ArcgisMcpFeatureTable>
export async function fetchFieldSummary(baseUrl, serviceUrl, field): Promise<ArcgisMcpFieldSummary>
// Health / diagnostics
export async function fetchMcpHealth(baseUrl): Promise<ArcgisMcpHealth>
Always use these wrappers rather than raw fetch() calls from components — they handle auth headers, error normalisation, and URL construction consistently.
src/utils/mcpGeoRenderer.ts
Renders GeoEntity objects as graphics on the live ArcGIS map.
Layer IDs (constants):
export const MCP_GEO_LAYER_ID = "mcp-geo-results";
export const MCP_GEO_SOURCE_LAYER_ID = "mcp-geo-source-results";
export const MCP_GEO_INTERACTIVE_LAYER_ID = "mcp-geo-interactive-results";
GeoEntity union:
type GeoEntity = GeoPoint | GeoCountry | GeoRegion | GeoExtent | GeoNamedPlace;
Each entity has:
kind:"point" | "country" | "region" | "extent" | "named-place"origin:"source"(from MCP response)labelorname: display string- Optional
descriptionandcontext: GeoContext(summary, links, mcpFields)
Render entry points:
// Add/replace all MCP result layers on the map
export async function renderMcpGeoEntities(
mapComponent: HTMLElement,
entities: GeoEntity[],
renderToken: number,
): Promise<void>
// Remove all MCP geo layers (call on reset/new conversation)
export async function clearMcpGeoLayer(mapComponent: HTMLElement): Promise<void>
src/components/HubServerManager.tsx
React component for the server management UI. Uses Calcite components.
Props:
interface HubServerManagerProps {
hubBaseUrl: string; // e.g. "/api/mcp"
onServersChange?: (servers: HubServer[]) => void;
}
Hub server states: "stopped" | "starting" | "running" | "error"
Import path for HubServer type:
import type { HubServer } from "./components/HubServerManager";
Patterns & Rules
Adding a new MCP server transport
- Add a new branch in
hub/server.tsconnectServer()— create the appropriateTransportinstance - Update
ServerConfig.transportunion type - Update the
HubServerManager.tsxform to expose the new transport fields - Add a sample entry in
mcp-hub.config.example.json
Adding a new MCP utility function to arcgisMcp.ts
- Define the response interface at the top of the file
- Use the existing
buildUrl+fetchJson<T>private helpers - Export only the public function — keep implementation details private
- Add the return-type interface to exports so callers can import it
Extending the geo renderer
- Add the new
GeoEntityvariant interface tomcpGeoRenderer.ts - Extend the
GeoEntityunion type - Add a render branch in
renderMcpGeoEntitiesfor the new kind - Ensure
deriveGeoEntitiesinmcpAgentCore.tspopulates the new variant
Debugging MCP tool discovery failures
- Check
/api/mcp/servers— confirm the serverstatusis"running" - Check
/api/mcp/tools— confirm tools appear with the rightserverId - Check
MCP_DISCOVERY_TIMEOUT_MS— increase if the server is slow to respond - Check transport: for
"url"mode, confirm the endpoint acceptsStreamableHTTPClientTransport, not SSE-only
Zod schema generation from inputSchema
When registering discovered MCP tools as LangChain tools via tool():
- Parse
inputSchema.propertiesand map each field to the correct Zod type - Handle
anyOf: [{ type: "string" }, { type: "null" }]→z.string().nullable() - Handle arrays of objects →
z.array(z.object({...})) - Handle nested
properties→ nestedz.object({...}) - Never collapse structured schemas to
z.string()— this breaks the LLM's ability to call the tool correctly
Environment variables
| Variable | Purpose | Default |
|---|---|---|
VITE_MCP_BASE_URL |
Hub proxy base URL for the client | /api/mcp |
VITE_ARCGIS_MCP_BASE_URL |
Legacy alias (still supported) | — |
MCP_HUB_PORT |
Port for the hub Express server | 3001 |
Output Format
- Show the exact file(s) to change with minimal diff context
- Flag any timeout constants, abort controller wiring, or LangGraph state references that need updating
- Highlight any
McpToolDef.inputSchema→ Zod mapping that must preserve nested structure - If adding a new route to the hub, list the HTTP method, path, and body/response shape
- If changing
ServerConfig, note whethermcp-hub.config.example.jsonalso needs updating