import { createHmac, timingSafeEqual } from "node:crypto"; const MCP_PROTOCOL_VERSION = "2025-06-18"; const MAX_BODY_BYTES = 1024 * 1024; function sendJson(response, statusCode, payload) { response.writeHead(statusCode, { "cache-control": "no-store", "content-type": "application/json; charset=utf-8", }); response.end(JSON.stringify(payload)); } async function readJsonBody(request, maxBytes = MAX_BODY_BYTES) { const chunks = []; let size = 0; for await (const chunk of request) { size += chunk.length; if (size > maxBytes) throw new Error("payload_too_large"); chunks.push(chunk); } return JSON.parse(Buffer.concat(chunks).toString("utf8")); } function timingSafeTokenEqual(left, right) { const leftBuffer = Buffer.from(String(left || "")); const rightBuffer = Buffer.from(String(right || "")); return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer); } function bearerToken(request) { const value = String(request.headers.authorization || ""); return value.startsWith("Bearer ") ? value.slice("Bearer ".length).trim() : ""; } function originAllowed(request, allowedOrigins) { const origin = String(request.headers.origin || "").trim(); if (!origin) return true; return allowedOrigins.has(origin); } function base64UrlJson(value) { return Buffer.from(JSON.stringify(value), "utf8").toString("base64url"); } function parseBase64UrlJson(value) { try { return JSON.parse(Buffer.from(value, "base64url").toString("utf8")); } catch { return null; } } function signCapability(encodedPayload, secret) { return createHmac("sha256", secret) .update(`nodedc.module-foundry.mcp-capability.v1.${encodedPayload}`) .digest("base64url"); } function mintMcpCapability(identity, config) { const now = Date.now(); const payload = { schemaVersion: "nodedc.module-foundry.mcp-capability.v1", service: "module-foundry", actorId: identity.actorId, ownerKey: identity.ownerKey, issuedAt: now, expiresAt: now + config.mcpCapabilityTtlMs, }; const encodedPayload = base64UrlJson(payload); return `fnd1.${encodedPayload}.${signCapability(encodedPayload, config.capabilitySecret)}`; } function actorFromMcpCapability(token, config) { const [prefix, encodedPayload, signature, ...rest] = String(token || "").split("."); if (prefix !== "fnd1" || !encodedPayload || !signature || rest.length || !config.capabilitySecret) return null; const expectedSignature = signCapability(encodedPayload, config.capabilitySecret); if (!timingSafeTokenEqual(signature, expectedSignature)) return null; const payload = parseBase64UrlJson(encodedPayload); if (!payload || payload.schemaVersion !== "nodedc.module-foundry.mcp-capability.v1" || payload.service !== "module-foundry") return null; const actorId = String(payload.actorId || "").trim(); const ownerKey = String(payload.ownerKey || "").trim(); const expiresAt = Number(payload.expiresAt); const issuedAt = Number(payload.issuedAt); if (!actorId || !ownerKey || !Number.isFinite(expiresAt) || !Number.isFinite(issuedAt)) return null; if (issuedAt > Date.now() + 60_000 || expiresAt <= Date.now() || expiresAt - issuedAt > config.mcpCapabilityTtlMs + 60_000) return null; return { actorId, ownerKey }; } function mcpError(id, code, message, data) { return { jsonrpc: "2.0", id: id ?? null, error: { code, message, ...(data === undefined ? {} : { data }), }, }; } function mcpResult(id, result) { return { jsonrpc: "2.0", id: id ?? null, result }; } function toolText(payload, isError = false) { return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], structuredContent: payload, ...(isError ? { isError: true } : {}), }; } function normalizeMcpError(error) { const code = String(error?.message || "foundry_operation_failed"); if (code === "application_not_found") return { code, status: 404 }; if (/^(?:data_product_|resync_required$|stream_cursor_invalid$)/.test(code) && Number.isInteger(error?.statusCode)) { return { code, status: error.statusCode }; } if (code.startsWith("invalid_") || code.startsWith("unknown_") || code.startsWith("map_") || code.endsWith("_required")) return { code, status: 400 }; if (code.startsWith("duplicate_") || code.includes("conflict") || code.includes("mismatch")) return { code, status: 409 }; return { code: "foundry_operation_failed", status: 500 }; } const mapPresentationProfileInputSchema = { type: "object", additionalProperties: false, required: [ "id", "version", "title", "semanticTypes", "label", "target", "facets", "styles", "classes", "defaultClassId", "sort", ], properties: { id: { type: "string", description: "Stable provider-neutral map.style_profile id." }, version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" }, title: { type: "string" }, semanticTypes: { type: "array", minItems: 1, maxItems: 8, uniqueItems: true, items: { type: "string" }, }, label: { type: "object", additionalProperties: false, required: [ "mode", "fields", "fontWeight", "sizePx", "color", "outlineColor", "outlineWidthPx", "backgroundColor", "backgroundOpacity", "paddingX", "paddingY", "maxLength", "offsetX", "offsetY", "hideCameraHeightMeters", ], properties: { mode: { type: "string", enum: ["subject_id", "attributes", "none"] }, fields: { type: "array", minItems: 1, maxItems: 16, uniqueItems: true, description: "Provider-neutral fact attributes in fallback order, for example display_name, label, name, title.", items: { type: "string" }, }, fontWeight: { type: "integer", minimum: 400, maximum: 700 }, sizePx: { type: "number", minimum: 8, maximum: 32 }, color: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, outlineWidthPx: { type: "number", minimum: 0, maximum: 6 }, backgroundColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, backgroundOpacity: { type: "number", minimum: 0, maximum: 1 }, paddingX: { type: "number", minimum: 0, maximum: 40 }, paddingY: { type: "number", minimum: 0, maximum: 40 }, maxLength: { type: "integer", minimum: 8, maximum: 240 }, offsetX: { type: "number", minimum: -100, maximum: 100 }, offsetY: { type: "number", minimum: -100, maximum: 100 }, hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 }, }, }, target: { oneOf: [ { type: "object", additionalProperties: false, required: [ "variant", "stemHeightMeters", "headSizePx", "stemWidthPx", "outlineColor", "outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters", ], properties: { variant: { type: "string", enum: ["elevated-spike"] }, stemHeightMeters: { type: "number", minimum: 1, maximum: 100000 }, headSizePx: { type: "number", minimum: 1, maximum: 64 }, stemWidthPx: { type: "number", minimum: 0.25, maximum: 16 }, outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, outlineOpacity: { type: "number", minimum: 0, maximum: 1 }, outlineWidthPx: { type: "number", minimum: 0, maximum: 8 }, hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 }, }, }, { type: "object", additionalProperties: false, required: ["variant", "outlineColor", "outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters"], properties: { variant: { type: "string", enum: ["surface-fill"], description: "Provider-neutral filled surface interpreted by the active Map adapter.", }, outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, outlineOpacity: { type: "number", minimum: 0, maximum: 1 }, outlineWidthPx: { type: "number", minimum: 0, maximum: 8 }, hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 }, }, }, ], }, facets: { type: "array", minItems: 1, maxItems: 16, items: { type: "object", additionalProperties: false, required: ["id", "field", "label", "filterable", "counter", "values"], properties: { id: { type: "string" }, field: { type: "string", description: "Normalized provider-neutral Data Product field." }, label: { type: "string" }, filterable: { type: "boolean" }, counter: { type: "boolean" }, values: { type: "array", minItems: 1, maxItems: 32, items: { type: "object", additionalProperties: false, required: ["value", "label", "order"], properties: { value: { type: "string" }, label: { type: "string" }, order: { type: "integer", minimum: 0, maximum: 1000 }, }, }, }, }, }, }, styles: { type: "array", minItems: 1, maxItems: 32, items: { type: "object", additionalProperties: false, required: ["id", "color", "opacity"], properties: { id: { type: "string" }, color: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, opacity: { type: "number", minimum: 0, maximum: 1 }, }, }, }, classes: { type: "array", minItems: 1, maxItems: 64, items: { type: "object", additionalProperties: false, required: ["id", "label", "priority", "match", "styleId", "renderable"], properties: { id: { type: "string" }, label: { type: "string" }, priority: { type: "integer", minimum: -10000, maximum: 10000 }, match: { type: "array", maxItems: 8, description: "All conditions use declared normalized facet fields; an empty list is the fallback class.", items: { type: "object", additionalProperties: false, required: ["field", "equals"], properties: { field: { type: "string" }, equals: { type: "string" } }, }, }, styleId: { type: "string" }, renderable: { type: "boolean" }, }, }, }, defaultClassId: { type: "string" }, sort: { type: "array", maxItems: 16, items: { type: "object", additionalProperties: false, required: ["field", "order"], properties: { field: { type: "string" }, order: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string" } }, }, }, }, }, }; const mapSubjectDetailProfileFieldInputSchema = { type: "object", additionalProperties: false, required: ["id", "source", "field", "label", "format"], properties: { id: { type: "string", description: "Stable field presentation id." }, aspectId: { type: "string", description: "Provider-neutral subject aspect id; defaults to primary." }, dataClass: { type: "string", enum: ["operational", "restricted"], description: "Explicit field presentation class. Restricted identifiers require a restricted joined aspect.", }, source: { type: "string", enum: ["fact", "attribute", "geometry", "context"] }, field: { type: "string", description: "Exact registered fact, context, geometry or provider-neutral attribute field." }, label: { type: "string", minLength: 1, maxLength: 100 }, format: { type: "string", enum: ["text", "number", "timestamp", "boolean", "coordinate", "signal_state", "movement_state", "telemetry_readings"], }, unit: { type: "string", minLength: 1, maxLength: 24 }, allowedReadingIds: { type: "array", maxItems: 256, uniqueItems: true, description: "Explicit classified sensor ids. Empty means no dynamic readings are rendered.", items: { type: "string" }, }, }, }; const mapSubjectDetailProfileInputSchema = { type: "object", additionalProperties: false, required: ["id", "version", "title", "semanticTypes", "defaultTabId", "tabs"], properties: { id: { type: "string", description: "Stable provider-neutral subject.detail_profile id." }, version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" }, title: { type: "string", minLength: 1, maxLength: 120 }, semanticTypes: { type: "array", minItems: 1, maxItems: 8, uniqueItems: true, items: { type: "string" } }, defaultTabId: { type: "string" }, tabs: { type: "array", minItems: 1, maxItems: 12, items: { type: "object", additionalProperties: false, required: ["id", "label", "emptyMessage", "sections"], properties: { id: { type: "string" }, label: { type: "string", minLength: 1, maxLength: 80 }, emptyMessage: { type: "string", minLength: 1, maxLength: 240 }, sections: { type: "array", maxItems: 12, items: { type: "object", additionalProperties: false, required: ["id", "label", "fields"], properties: { id: { type: "string" }, label: { type: "string", minLength: 1, maxLength: 80 }, fields: { type: "array", maxItems: 32, items: mapSubjectDetailProfileFieldInputSchema }, }, }, }, }, }, }, }, }; const mapPageSettingsPatchInputSchema = { type: "object", additionalProperties: false, minProperties: 1, properties: { imagerySource: { type: "string" }, imageryVisible: { type: "boolean" }, cacheEnabled: { type: "boolean" }, cacheNoOverwrite: { type: "boolean" }, terrainEnabled: { type: "boolean" }, terrainExaggeration: { type: "number" }, monochrome: { type: "boolean" }, monochromeColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, imageryGamma: { type: "number" }, imageryHue: { type: "number" }, imageryAlpha: { type: "number" }, globeColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, backgroundColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, atmosphereEnabled: { type: "boolean" }, atmosphereHue: { type: "number" }, atmosphereSaturation: { type: "number" }, atmosphereBrightness: { type: "number" }, fogEnabled: { type: "boolean" }, fogDensity: { type: "number" }, sunEnabled: { type: "boolean" }, sunHour: { type: "number" }, sunIntensity: { type: "number" }, shadowsEnabled: { type: "boolean" }, buildingsVisible: { type: "boolean" }, buildingsColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, buildingsOpacity: { type: "number" }, buildingsDetail: { type: "number" }, imageryBrightness: { type: "number" }, imageryContrast: { type: "number" }, imagerySaturation: { type: "number" }, gridVisible: { type: "boolean" }, gridLodEnabled: { type: "boolean" }, gridHeightMeters: { type: "number" }, gridLod1MaxHeightKm: { type: "number" }, gridLod1StepKm: { type: "number" }, gridLod2MaxHeightKm: { type: "number" }, gridLod2StepKm: { type: "number" }, gridLod3StepKm: { type: "number" }, gridRadiusKm: { type: "number" }, gridLineWidth: { type: "number" }, gridColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, gridOpacity: { type: "number" }, gridDotsEnabled: { type: "boolean" }, gridDotsSize: { type: "number" }, gridDotsColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" }, gridDotsOpacity: { type: "number" }, }, }; const mapSubjectStateInputSchema = { type: "object", additionalProperties: false, required: ["bindingId", "visible", "filters", "window"], properties: { bindingId: { type: "string", description: "Stable Map Data Product binding id. Editable labels are never state keys." }, visible: { type: "boolean", description: "False is an explicit empty map state and must not be normalized to all." }, filters: { type: "object", description: "Facet selections keyed by normalized field. Missing field is unconstrained; an empty array matches nothing.", additionalProperties: { type: "array", uniqueItems: true, items: { type: "string" } }, }, window: { type: "object", additionalProperties: false, required: ["open", "rect", "maximized", "zIndex"], properties: { open: { type: "boolean" }, rect: { type: "object", additionalProperties: false, required: ["x", "y", "width", "height"], properties: { x: { type: "number" }, y: { type: "number" }, width: { type: "number", minimum: 200 }, height: { type: "number", minimum: 160 }, }, }, maximized: { type: "boolean" }, zIndex: { type: "integer", minimum: 1 }, }, }, }, }; const tools = [ { name: "foundry_status", title: "NDC Module Foundry status", description: "Read the availability and baseline scope of NDC Module Foundry. Canonical Page Library templates are read-only.", inputSchema: { type: "object", additionalProperties: false, properties: {} }, }, { name: "foundry_list_applications", title: "List application instances", description: "List editable application instances in NDC Module Foundry. This does not mutate Page Library.", inputSchema: { type: "object", additionalProperties: false, properties: {} }, }, { name: "foundry_get_application", title: "Get application instance", description: "Read one editable application instance, its page instances, features, map presentation profiles and bindings.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId"], properties: { applicationId: { type: "string", description: "UUID of an application instance." } }, }, }, { name: "foundry_create_application", title: "Create application instance", description: "Create a draft application instance from a registered page template. The canonical template itself remains unchanged. Application deletion is deliberately unavailable.", inputSchema: { type: "object", additionalProperties: false, required: ["name", "idempotencyKey"], properties: { name: { type: "string", description: "Human-readable application name." }, idempotencyKey: { type: "string", description: "Stable key for replay-safe writes." }, slug: { type: "string" }, description: { type: "string" }, templateId: { type: "string", description: "Registered Page Library template id. Defaults to map." }, templateVersion: { type: "string" }, theme: { type: "string", enum: ["dark", "light"] }, }, }, }, { name: "foundry_update_application_metadata", title: "Update application metadata", description: "Rename or describe an application instance. It cannot change canonical templates or delete an application.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "idempotencyKey"], properties: { applicationId: { type: "string" }, idempotencyKey: { type: "string" }, name: { type: "string" }, slug: { type: "string" }, description: { type: "string" }, }, }, }, { name: "foundry_add_page_instance", title: "Add page instance to application", description: "Create one more instance of a registered Page Library template inside an application. A template may be used repeatedly; Page Library remains read-only.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "templateId", "idempotencyKey"], properties: { applicationId: { type: "string" }, templateId: { type: "string", description: "Registered Page Library template id." }, templateVersion: { type: "string" }, idempotencyKey: { type: "string" }, title: { type: "string" }, navigationLabel: { type: "string" }, }, }, }, { name: "foundry_upsert_map_pin_binding", title: "Upsert map pin binding", description: "Create or update a visual map-pin binding on a Map Page instance. This stores a stable visual binding; it does not call a provider, Engine or an external data source.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "idempotencyKey", "binding"], properties: { applicationId: { type: "string" }, pageId: { type: "string", description: "Map Page instance id inside the application." }, idempotencyKey: { type: "string" }, binding: { type: "object", description: "Provider-neutral map-pin binding. Coordinates and source entity are mandatory.", required: ["id", "subjectId", "coordinates", "source"], properties: { id: { type: "string" }, subjectId: { type: "string" }, label: { type: "string" }, status: { type: "string" }, coordinates: { type: "object", required: ["longitude", "latitude"], properties: { longitude: { type: "number" }, latitude: { type: "number" }, heightMeters: { type: "number" }, }, }, source: { type: "object", required: ["entityId"], properties: { entityId: { type: "string" }, streamId: { type: "string" }, displayFields: { type: "array", items: { type: "string" } }, }, }, attributes: { type: "object" }, }, }, }, }, }, { name: "foundry_remove_map_pin_binding", title: "Remove map pin binding", description: "Remove one obsolete visual map-pin binding from a Map Page instance. The operation does not affect data-product consumers or the canonical Page Library.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "bindingId", "idempotencyKey"], properties: { applicationId: { type: "string" }, pageId: { type: "string", description: "Map Page instance id inside the application." }, bindingId: { type: "string", description: "Stable id of the obsolete visual map-pin binding." }, idempotencyKey: { type: "string" }, }, }, }, { name: "foundry_upsert_map_presentation_profile", title: "Upsert Map presentation profile", description: "Create or update a versioned provider-neutral Map presentation profile. The profile owns target geometry, state classes, filters, counters, sort order and labels; it contains no provider transport or credentials.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "idempotencyKey", "profile"], properties: { applicationId: { type: "string" }, pageId: { type: "string", description: "Map Page instance id inside the application." }, idempotencyKey: { type: "string" }, profile: { ...mapPresentationProfileInputSchema, description: "Versioned provider-neutral map.style_profile. Classification uses declared normalized facets, never provider-specific raw status values.", }, }, }, }, { name: "foundry_upsert_map_subject_detail_profile", title: "Upsert Map subject detail profile", description: "Create or update a versioned provider-neutral subject detail profile. Tabs and fields are exact, fail-closed presentation declarations; provider transport and unrestricted payload rendering are forbidden.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "idempotencyKey", "profile"], properties: { applicationId: { type: "string" }, pageId: { type: "string", description: "Map Page instance id inside the application." }, idempotencyKey: { type: "string" }, profile: mapSubjectDetailProfileInputSchema, }, }, }, { name: "foundry_update_map_page_settings", title: "Update Application Map settings", description: "Update visual environment settings on one Map page instance inside one Application. This operation never modifies the canonical Page Library template.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "idempotencyKey", "settings"], properties: { applicationId: { type: "string" }, pageId: { type: "string", description: "Map page instance id inside the editable Application." }, idempotencyKey: { type: "string" }, settings: mapPageSettingsPatchInputSchema, }, }, }, { name: "foundry_save_map_page_view_state", title: "Save Application Map view state", description: "Persist one complete current Map view through the Application Save path: camera, map geometry, base settings, exact subject visibility/facets and every binding window geometry/z-order. Stable identity is bindingId.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "idempotencyKey", "viewState"], properties: { applicationId: { type: "string" }, pageId: { type: "string", description: "Map Page instance id inside the editable Application." }, idempotencyKey: { type: "string" }, viewState: { type: "object", additionalProperties: false, required: ["mapHeight", "camera", "subjectStates"], properties: { settings: mapPageSettingsPatchInputSchema, mapHeight: { type: "integer", minimum: 360, maximum: 5000 }, camera: { type: "object", additionalProperties: false, required: ["longitude", "latitude", "height", "heading", "pitch", "roll"], properties: { longitude: { type: "number" }, latitude: { type: "number" }, height: { type: "number" }, heading: { type: "number" }, pitch: { type: "number" }, roll: { type: "number" }, }, }, subjectStates: { type: "array", maxItems: 64, items: mapSubjectStateInputSchema }, }, }, }, }, }, { name: "foundry_upsert_map_data_product_binding", title: "Upsert Map data product binding", description: "Bind one approved provider-neutral data product to a live-data slot (points or zones) of a Map Page instance. The binding stores no provider transport, endpoint, tenant, connection or credential.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "idempotencyKey", "binding"], properties: { applicationId: { type: "string" }, pageId: { type: "string", description: "Map Page instance id inside the application." }, idempotencyKey: { type: "string" }, binding: { type: "object", required: ["id", "dataProductId", "slotId", "semanticTypes"], properties: { id: { type: "string" }, displayName: { type: "string", minLength: 1, maxLength: 120, description: "User-defined text label shown in the Objects menu and window header." }, order: { type: "integer", minimum: 0, maximum: 10000, description: "Application composition order inside the Objects menu." }, dataProductId: { type: "string", description: "Versioned provider-neutral product, for example fleet.positions.current.v1." }, slotId: { type: "string", description: "Approved Map Page live-data slot (entity-stream or zone-stream)." }, semanticTypes: { type: "array", items: { type: "string" } }, fieldProjection: { type: "array", items: { type: "string" } }, presentationProfileId: { type: "string", description: "Existing page-owned provider-neutral Map presentation profile id." }, subjectDetailProfileId: { type: "string", description: "Existing page-owned provider-neutral subject detail profile id." }, aspectId: { type: "string", description: "Stable provider-neutral aspect id inside the selected subject composition." }, joinToBindingId: { type: "string", description: "Primary Map binding whose stable sourceId is used to join this subject-details aspect." }, dataClass: { type: "string", enum: ["operational", "restricted"], description: "Declared data class, verified against the versioned Foundry consumer policy before runtime.", }, }, }, }, }, }, { name: "foundry_plan_map_data_product_consumer", title: "Plan Map data product consumer", description: "Validate an approved Map data-product binding, its target-scoped server reader and versioned Foundry consumer policy. Returns a safe deterministic plan; it never returns a capability or endpoint.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "bindingId"], properties: { applicationId: { type: "string" }, pageId: { type: "string" }, bindingId: { type: "string" }, }, }, }, { name: "foundry_apply_map_data_product_consumer", title: "Apply Map data product consumer", description: "Apply an exact consumer plan and commit a scoped snapshot into server-owned Foundry state. Active page viewers then share one upstream durable patch stream.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "bindingId", "planId", "idempotencyKey"], properties: { applicationId: { type: "string" }, pageId: { type: "string" }, bindingId: { type: "string" }, planId: { type: "string" }, idempotencyKey: { type: "string" }, }, }, }, { name: "foundry_get_map_data_product_consumer_status", title: "Get Map data product consumer status", description: "Read safe persisted cursor, subject/status counters, reconnect diagnostics and viewer/upstream counts for one approved binding. No raw fact attributes, capability or endpoint are returned.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "bindingId"], properties: { applicationId: { type: "string" }, pageId: { type: "string" }, bindingId: { type: "string" }, }, }, }, { name: "foundry_accept_map_data_product_consumer", title: "Accept Map data product consumer", description: "Run a bounded lifecycle acceptance lease against the shared server consumer and verify persisted snapshot/cursor, subject identity, optional new patches, deduplication and secret boundary.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "bindingId"], properties: { applicationId: { type: "string" }, pageId: { type: "string" }, bindingId: { type: "string" }, timeoutMs: { type: "integer", minimum: 250, maximum: 30000 }, minSubjectCount: { type: "integer", minimum: 0, maximum: 5000 }, minPatchCount: { type: "integer", minimum: 0, maximum: 1000 }, }, }, }, { name: "foundry_rollback_map_data_product_consumer", title: "Rollback Map data product consumer", description: "Stop one server-owned consumer without deleting its last safe snapshot. Re-applying a fresh exact plan resumes it.", inputSchema: { type: "object", additionalProperties: false, required: ["applicationId", "pageId", "bindingId", "idempotencyKey"], properties: { applicationId: { type: "string" }, pageId: { type: "string" }, bindingId: { type: "string" }, idempotencyKey: { type: "string" }, }, }, }, ]; function toolMap(operations) { return { foundry_status: () => operations.status(), foundry_list_applications: () => operations.listApplications(), foundry_get_application: (input) => operations.getApplication(input.applicationId), foundry_create_application: (input, actor) => operations.createApplication(input, actor), foundry_update_application_metadata: (input, actor) => operations.updateApplicationMetadata(input, actor), foundry_add_page_instance: (input, actor) => operations.addPageInstance(input, actor), foundry_upsert_map_pin_binding: (input, actor) => operations.upsertMapPinBinding(input, actor), foundry_remove_map_pin_binding: (input, actor) => operations.removeMapPinBinding(input, actor), foundry_upsert_map_presentation_profile: (input, actor) => operations.upsertMapPresentationProfile(input, actor), foundry_upsert_map_subject_detail_profile: (input, actor) => operations.upsertMapSubjectDetailProfile(input, actor), foundry_update_map_page_settings: (input, actor) => operations.updateMapPageSettings(input, actor), foundry_save_map_page_view_state: (input, actor) => operations.saveMapPageViewState(input, actor), foundry_upsert_map_data_product_binding: (input, actor) => operations.upsertMapDataProductBinding(input, actor), foundry_plan_map_data_product_consumer: (input) => operations.planMapDataProductConsumer(input), foundry_apply_map_data_product_consumer: (input, actor) => operations.applyMapDataProductConsumer(input, actor), foundry_get_map_data_product_consumer_status: (input) => operations.getMapDataProductConsumerStatus(input), foundry_accept_map_data_product_consumer: (input) => operations.acceptMapDataProductConsumer(input), foundry_rollback_map_data_product_consumer: (input, actor) => operations.rollbackMapDataProductConsumer(input, actor), }; } export async function handleFoundryMcpRequest(request, response, options) { const config = options.getConfig(); const allowedOrigins = new Set(config.mcpAllowedOrigins || []); if (request.method !== "POST") { response.writeHead(405, { Allow: "POST" }); response.end(); return; } if (!originAllowed(request, allowedOrigins)) return sendJson(response, 403, { error: "mcp_origin_not_allowed" }); const token = bearerToken(request); let actor = config.capabilitySecret ? actorFromMcpCapability(token, config) : null; if (!actor && typeof options.authenticateAgentToken === "function") { actor = await options.authenticateAgentToken(token, { check: true }); } if (!config.capabilitySecret && typeof options.authenticateAgentToken !== "function") { return sendJson(response, 503, { error: "foundry_mcp_not_configured" }); } if (!actor) return sendJson(response, 401, { error: "mcp_unauthorized" }); let message; try { message = await readJsonBody(request); } catch (error) { return sendJson(response, error?.message === "payload_too_large" ? 413 : 400, { error: error?.message === "payload_too_large" ? "payload_too_large" : "invalid_json" }); } if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") { return sendJson(response, 400, mcpError(message?.id, -32600, "Invalid Request")); } const id = message.id; if (message.method === "initialize") { return sendJson(response, 200, mcpResult(id, { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: { tools: { listChanged: false } }, serverInfo: { name: "nodedc_module_foundry", version: "0.6.0" }, instructions: "NDC Module Foundry edits application instances, provider-neutral map presentation profiles and approved server-owned data-product consumers. Page Library is read-only. Application deletion is unavailable. Provider endpoints and credentials are never MCP inputs or outputs.", })); } if (message.method === "notifications/initialized") return sendJson(response, 202, {}); if (message.method === "ping") return sendJson(response, 200, mcpResult(id, {})); if (message.method === "tools/list") return sendJson(response, 200, mcpResult(id, { tools })); if (message.method !== "tools/call") return sendJson(response, 200, mcpError(id, -32601, "Method not found")); const params = message.params && typeof message.params === "object" ? message.params : {}; const name = String(params.name || ""); const handler = toolMap(options.operations)[name]; if (!handler) return sendJson(response, 200, mcpError(id, -32602, "Unknown tool")); try { const result = await handler(params.arguments && typeof params.arguments === "object" ? params.arguments : {}, actor); return sendJson(response, 200, mcpResult(id, toolText(result))); } catch (error) { const normalized = normalizeMcpError(error); return sendJson(response, 200, mcpResult(id, toolText({ error: normalized.code }, true))); } } function ownerIdentity(owner) { const source = owner && typeof owner === "object" ? owner : {}; const actorId = String(source.userId || source.user_id || source.id || "").trim(); const ownerKey = String(source.key || source.ownerKey || source.owner_key || actorId).trim(); const groups = [source.groups, source.roles, source.roleKeys] .flatMap((value) => Array.isArray(value) ? value : []) .map((value) => String(value || "").trim().toLowerCase()) .filter(Boolean); return { actorId, ownerKey, groups }; } function hasFoundryAccess(identity, config) { if (!identity.actorId || !identity.ownerKey) return false; // A Launcher/Authentik block revokes every Foundry surface, including a // previously discoverable AI Workspace entitlement. if (identity.groups.includes("nodedc:module-foundry:blocked")) return false; if (config.allowAllAuthenticated) return true; if (config.allowedOwnerIds.includes(identity.actorId) || config.allowedOwnerIds.includes(identity.ownerKey)) return true; return identity.groups.some((group) => config.allowedOwnerGroups.includes(group)); } export async function handleFoundryEntitlementRequest(request, response, options) { const config = options.getConfig(); if (request.method !== "POST") { response.writeHead(405, { Allow: "POST" }); response.end(); return; } if (!config.internalAccessToken || !config.capabilitySecret) return sendJson(response, 503, { ok: false, error: "foundry_entitlement_not_configured" }); if (!timingSafeTokenEqual(bearerToken(request), config.internalAccessToken)) return sendJson(response, 401, { ok: false, error: "entitlement_unauthorized" }); let body; try { body = await readJsonBody(request); } catch (error) { return sendJson(response, error?.message === "payload_too_large" ? 413 : 400, { ok: false, error: "invalid_json" }); } if (body?.schemaVersion !== "ai-workspace.entitlement-request.v1") return sendJson(response, 400, { ok: false, error: "unsupported_entitlement_schema" }); if (body?.appId !== "module-foundry") return sendJson(response, 400, { ok: false, error: "unsupported_entitlement_app" }); const identity = ownerIdentity(body.owner); const accessAllowed = hasFoundryAccess(identity, config); const grantBase = { appId: "module-foundry", appTitle: "NDC Module Foundry", surface: "module-foundry", scopes: [ "foundry.application.read", "foundry.application.create", "foundry.application.update", "foundry.page-instance.create", "foundry.map-pin.upsert", "foundry.map-pin.remove", "foundry.map-presentation-profile.upsert", "foundry.map-page-settings.update", "foundry.map-page-view-state.save", "foundry.map-data-product.upsert", "foundry.map-data-product-consumer.read", "foundry.map-data-product-consumer.lifecycle", ], }; if (!accessAllowed) { return sendJson(response, 200, { appGrants: { "module-foundry": { ...grantBase, enabled: false, denied: true, reason: "foundry_access_not_granted", deniedText: "NDC Module Foundry недоступен в текущем контексте.", mcpServers: [], }, }, }); } if (!config.mcpUrl) return sendJson(response, 503, { ok: false, error: "foundry_mcp_not_configured" }); const capability = mintMcpCapability(identity, config); return sendJson(response, 200, { appGrants: { "module-foundry": { ...grantBase, enabled: true, context: { actorId: identity.actorId }, mcpServers: [{ serverName: "nodedc_module_foundry", url: config.mcpUrl, enabled: true, required: false, startupTimeoutSec: 20, toolTimeoutSec: 60, httpHeaders: { Authorization: `Bearer ${capability}`, "MCP-Protocol-Version": MCP_PROTOCOL_VERSION, }, }], }, }, }); }