@pascal-app/mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +330 -0
  3. package/dist/bin/pascal-mcp.d.ts +3 -0
  4. package/dist/bin/pascal-mcp.d.ts.map +1 -0
  5. package/dist/bin/pascal-mcp.js +91 -0
  6. package/dist/bridge/index.d.ts +3 -0
  7. package/dist/bridge/index.d.ts.map +1 -0
  8. package/dist/bridge/index.js +1 -0
  9. package/dist/bridge/node-shims.d.ts +16 -0
  10. package/dist/bridge/node-shims.d.ts.map +1 -0
  11. package/dist/bridge/node-shims.js +25 -0
  12. package/dist/bridge/scene-bridge.d.ts +158 -0
  13. package/dist/bridge/scene-bridge.d.ts.map +1 -0
  14. package/dist/bridge/scene-bridge.js +482 -0
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +4 -0
  18. package/dist/lib/rehydrate-site-children.d.ts +12 -0
  19. package/dist/lib/rehydrate-site-children.d.ts.map +1 -0
  20. package/dist/lib/rehydrate-site-children.js +40 -0
  21. package/dist/lib/safe-fetch.d.ts +19 -0
  22. package/dist/lib/safe-fetch.d.ts.map +1 -0
  23. package/dist/lib/safe-fetch.js +221 -0
  24. package/dist/operations/index.d.ts +2 -0
  25. package/dist/operations/index.d.ts.map +1 -0
  26. package/dist/operations/index.js +1 -0
  27. package/dist/operations/scene-operations.d.ts +67 -0
  28. package/dist/operations/scene-operations.d.ts.map +1 -0
  29. package/dist/operations/scene-operations.js +192 -0
  30. package/dist/prompts/from-brief.d.ts +11 -0
  31. package/dist/prompts/from-brief.d.ts.map +1 -0
  32. package/dist/prompts/from-brief.js +48 -0
  33. package/dist/prompts/index.d.ts +10 -0
  34. package/dist/prompts/index.d.ts.map +1 -0
  35. package/dist/prompts/index.js +14 -0
  36. package/dist/prompts/iterate-on-feedback.d.ts +11 -0
  37. package/dist/prompts/iterate-on-feedback.d.ts.map +1 -0
  38. package/dist/prompts/iterate-on-feedback.js +41 -0
  39. package/dist/prompts/renovation-from-photos.d.ts +24 -0
  40. package/dist/prompts/renovation-from-photos.d.ts.map +1 -0
  41. package/dist/prompts/renovation-from-photos.js +135 -0
  42. package/dist/prompts/scene-guidance.d.ts +2 -0
  43. package/dist/prompts/scene-guidance.d.ts.map +1 -0
  44. package/dist/prompts/scene-guidance.js +24 -0
  45. package/dist/resources/agent-guide.d.ts +5 -0
  46. package/dist/resources/agent-guide.d.ts.map +1 -0
  47. package/dist/resources/agent-guide.js +85 -0
  48. package/dist/resources/catalog-items.d.ts +10 -0
  49. package/dist/resources/catalog-items.d.ts.map +1 -0
  50. package/dist/resources/catalog-items.js +29 -0
  51. package/dist/resources/constraints.d.ts +9 -0
  52. package/dist/resources/constraints.d.ts.map +1 -0
  53. package/dist/resources/constraints.js +69 -0
  54. package/dist/resources/index.d.ts +15 -0
  55. package/dist/resources/index.d.ts.map +1 -0
  56. package/dist/resources/index.js +23 -0
  57. package/dist/resources/scene-current.d.ts +9 -0
  58. package/dist/resources/scene-current.d.ts.map +1 -0
  59. package/dist/resources/scene-current.js +20 -0
  60. package/dist/resources/scene-summary.d.ts +10 -0
  61. package/dist/resources/scene-summary.d.ts.map +1 -0
  62. package/dist/resources/scene-summary.js +196 -0
  63. package/dist/server.d.ts +14 -0
  64. package/dist/server.d.ts.map +1 -0
  65. package/dist/server.js +18 -0
  66. package/dist/storage/index.d.ts +13 -0
  67. package/dist/storage/index.d.ts.map +1 -0
  68. package/dist/storage/index.js +14 -0
  69. package/dist/storage/slug.d.ts +21 -0
  70. package/dist/storage/slug.d.ts.map +1 -0
  71. package/dist/storage/slug.js +62 -0
  72. package/dist/storage/sqlite-driver.d.ts +18 -0
  73. package/dist/storage/sqlite-driver.d.ts.map +1 -0
  74. package/dist/storage/sqlite-driver.js +32 -0
  75. package/dist/storage/sqlite-scene-store.d.ts +51 -0
  76. package/dist/storage/sqlite-scene-store.d.ts.map +1 -0
  77. package/dist/storage/sqlite-scene-store.js +526 -0
  78. package/dist/storage/types.d.ts +136 -0
  79. package/dist/storage/types.d.ts.map +1 -0
  80. package/dist/storage/types.js +28 -0
  81. package/dist/templates/empty-studio.d.ts +8 -0
  82. package/dist/templates/empty-studio.d.ts.map +1 -0
  83. package/dist/templates/empty-studio.js +230 -0
  84. package/dist/templates/garden-house.d.ts +8 -0
  85. package/dist/templates/garden-house.d.ts.map +1 -0
  86. package/dist/templates/garden-house.js +242 -0
  87. package/dist/templates/index.d.ts +23 -0
  88. package/dist/templates/index.d.ts.map +1 -0
  89. package/dist/templates/index.js +20 -0
  90. package/dist/templates/two-bedroom.d.ts +8 -0
  91. package/dist/templates/two-bedroom.d.ts.map +1 -0
  92. package/dist/templates/two-bedroom.js +270 -0
  93. package/dist/tools/apply-patch.d.ts +25 -0
  94. package/dist/tools/apply-patch.d.ts.map +1 -0
  95. package/dist/tools/apply-patch.js +59 -0
  96. package/dist/tools/asset-catalog.d.ts +16 -0
  97. package/dist/tools/asset-catalog.d.ts.map +1 -0
  98. package/dist/tools/asset-catalog.js +308 -0
  99. package/dist/tools/check-collisions.d.ts +15 -0
  100. package/dist/tools/check-collisions.d.ts.map +1 -0
  101. package/dist/tools/check-collisions.js +61 -0
  102. package/dist/tools/construction-tools.d.ts +93 -0
  103. package/dist/tools/construction-tools.d.ts.map +1 -0
  104. package/dist/tools/construction-tools.js +369 -0
  105. package/dist/tools/create-level.d.ts +14 -0
  106. package/dist/tools/create-level.d.ts.map +1 -0
  107. package/dist/tools/create-level.js +48 -0
  108. package/dist/tools/create-wall.d.ts +15 -0
  109. package/dist/tools/create-wall.d.ts.map +1 -0
  110. package/dist/tools/create-wall.js +50 -0
  111. package/dist/tools/cut-opening.d.ts +18 -0
  112. package/dist/tools/cut-opening.d.ts.map +1 -0
  113. package/dist/tools/cut-opening.js +57 -0
  114. package/dist/tools/delete-node.d.ts +12 -0
  115. package/dist/tools/delete-node.d.ts.map +1 -0
  116. package/dist/tools/delete-node.js +37 -0
  117. package/dist/tools/describe-node.d.ts +17 -0
  118. package/dist/tools/describe-node.d.ts.map +1 -0
  119. package/dist/tools/describe-node.js +86 -0
  120. package/dist/tools/duplicate-level.d.ts +12 -0
  121. package/dist/tools/duplicate-level.d.ts.map +1 -0
  122. package/dist/tools/duplicate-level.js +56 -0
  123. package/dist/tools/errors.d.ts +20 -0
  124. package/dist/tools/errors.d.ts.map +1 -0
  125. package/dist/tools/errors.js +25 -0
  126. package/dist/tools/export-glb.d.ts +10 -0
  127. package/dist/tools/export-glb.d.ts.map +1 -0
  128. package/dist/tools/export-glb.js +24 -0
  129. package/dist/tools/export-json.d.ts +11 -0
  130. package/dist/tools/export-json.d.ts.map +1 -0
  131. package/dist/tools/export-json.js +23 -0
  132. package/dist/tools/find-nodes.d.ts +32 -0
  133. package/dist/tools/find-nodes.d.ts.map +1 -0
  134. package/dist/tools/find-nodes.js +105 -0
  135. package/dist/tools/geometry.d.ts +24 -0
  136. package/dist/tools/geometry.d.ts.map +1 -0
  137. package/dist/tools/geometry.js +94 -0
  138. package/dist/tools/get-node.d.ts +11 -0
  139. package/dist/tools/get-node.d.ts.map +1 -0
  140. package/dist/tools/get-node.js +27 -0
  141. package/dist/tools/get-scene.d.ts +11 -0
  142. package/dist/tools/get-scene.d.ts.map +1 -0
  143. package/dist/tools/get-scene.js +26 -0
  144. package/dist/tools/index.d.ts +12 -0
  145. package/dist/tools/index.d.ts.map +1 -0
  146. package/dist/tools/index.js +64 -0
  147. package/dist/tools/live-sync.d.ts +11 -0
  148. package/dist/tools/live-sync.d.ts.map +1 -0
  149. package/dist/tools/live-sync.js +62 -0
  150. package/dist/tools/measure.d.ts +14 -0
  151. package/dist/tools/measure.d.ts.map +1 -0
  152. package/dist/tools/measure.js +137 -0
  153. package/dist/tools/photo-to-scene/index.d.ts +11 -0
  154. package/dist/tools/photo-to-scene/index.d.ts.map +1 -0
  155. package/dist/tools/photo-to-scene/index.js +11 -0
  156. package/dist/tools/photo-to-scene/photo-to-scene.d.ts +26 -0
  157. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -0
  158. package/dist/tools/photo-to-scene/photo-to-scene.js +323 -0
  159. package/dist/tools/place-item.d.ts +15 -0
  160. package/dist/tools/place-item.d.ts.map +1 -0
  161. package/dist/tools/place-item.js +84 -0
  162. package/dist/tools/redo.d.ts +11 -0
  163. package/dist/tools/redo.d.ts.map +1 -0
  164. package/dist/tools/redo.js +25 -0
  165. package/dist/tools/room-tools.d.ts +97 -0
  166. package/dist/tools/room-tools.d.ts.map +1 -0
  167. package/dist/tools/room-tools.js +510 -0
  168. package/dist/tools/scene-lifecycle/create-project.d.ts +33 -0
  169. package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -0
  170. package/dist/tools/scene-lifecycle/create-project.js +70 -0
  171. package/dist/tools/scene-lifecycle/delete-scene.d.ts +12 -0
  172. package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -0
  173. package/dist/tools/scene-lifecycle/delete-scene.js +42 -0
  174. package/dist/tools/scene-lifecycle/get-project-status.d.ts +31 -0
  175. package/dist/tools/scene-lifecycle/get-project-status.d.ts.map +1 -0
  176. package/dist/tools/scene-lifecycle/get-project-status.js +69 -0
  177. package/dist/tools/scene-lifecycle/index.d.ts +17 -0
  178. package/dist/tools/scene-lifecycle/index.d.ts.map +1 -0
  179. package/dist/tools/scene-lifecycle/index.js +29 -0
  180. package/dist/tools/scene-lifecycle/list-scenes.d.ts +27 -0
  181. package/dist/tools/scene-lifecycle/list-scenes.d.ts.map +1 -0
  182. package/dist/tools/scene-lifecycle/list-scenes.js +49 -0
  183. package/dist/tools/scene-lifecycle/load-scene.d.ts +31 -0
  184. package/dist/tools/scene-lifecycle/load-scene.d.ts.map +1 -0
  185. package/dist/tools/scene-lifecycle/load-scene.js +55 -0
  186. package/dist/tools/scene-lifecycle/metadata.d.ts +49 -0
  187. package/dist/tools/scene-lifecycle/metadata.d.ts.map +1 -0
  188. package/dist/tools/scene-lifecycle/metadata.js +68 -0
  189. package/dist/tools/scene-lifecycle/rename-scene.d.ts +22 -0
  190. package/dist/tools/scene-lifecycle/rename-scene.d.ts.map +1 -0
  191. package/dist/tools/scene-lifecycle/rename-scene.js +63 -0
  192. package/dist/tools/scene-lifecycle/save-scene.d.ts +42 -0
  193. package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -0
  194. package/dist/tools/scene-lifecycle/save-scene.js +130 -0
  195. package/dist/tools/scene-lifecycle/test-utils.d.ts +39 -0
  196. package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -0
  197. package/dist/tools/scene-lifecycle/test-utils.js +211 -0
  198. package/dist/tools/scene-query.d.ts +57 -0
  199. package/dist/tools/scene-query.d.ts.map +1 -0
  200. package/dist/tools/scene-query.js +645 -0
  201. package/dist/tools/schemas.d.ts +52 -0
  202. package/dist/tools/schemas.d.ts.map +1 -0
  203. package/dist/tools/schemas.js +42 -0
  204. package/dist/tools/set-zone.d.ts +14 -0
  205. package/dist/tools/set-zone.d.ts.map +1 -0
  206. package/dist/tools/set-zone.js +48 -0
  207. package/dist/tools/templates/create-from-template.d.ts +54 -0
  208. package/dist/tools/templates/create-from-template.d.ts.map +1 -0
  209. package/dist/tools/templates/create-from-template.js +145 -0
  210. package/dist/tools/templates/create-house-from-brief.d.ts +35 -0
  211. package/dist/tools/templates/create-house-from-brief.d.ts.map +1 -0
  212. package/dist/tools/templates/create-house-from-brief.js +170 -0
  213. package/dist/tools/templates/index.d.ts +14 -0
  214. package/dist/tools/templates/index.d.ts.map +1 -0
  215. package/dist/tools/templates/index.js +18 -0
  216. package/dist/tools/templates/list-templates.d.ts +18 -0
  217. package/dist/tools/templates/list-templates.d.ts.map +1 -0
  218. package/dist/tools/templates/list-templates.js +36 -0
  219. package/dist/tools/undo.d.ts +11 -0
  220. package/dist/tools/undo.d.ts.map +1 -0
  221. package/dist/tools/undo.js +25 -0
  222. package/dist/tools/validate-scene.d.ts +14 -0
  223. package/dist/tools/validate-scene.d.ts.map +1 -0
  224. package/dist/tools/validate-scene.js +24 -0
  225. package/dist/tools/variants/generate-variants.d.ts +30 -0
  226. package/dist/tools/variants/generate-variants.d.ts.map +1 -0
  227. package/dist/tools/variants/generate-variants.js +160 -0
  228. package/dist/tools/variants/index.d.ts +9 -0
  229. package/dist/tools/variants/index.d.ts.map +1 -0
  230. package/dist/tools/variants/index.js +9 -0
  231. package/dist/tools/variants/mutations.d.ts +18 -0
  232. package/dist/tools/variants/mutations.d.ts.map +1 -0
  233. package/dist/tools/variants/mutations.js +324 -0
  234. package/dist/tools/vision/analyze-floorplan-image.d.ts +33 -0
  235. package/dist/tools/vision/analyze-floorplan-image.d.ts.map +1 -0
  236. package/dist/tools/vision/analyze-floorplan-image.js +146 -0
  237. package/dist/tools/vision/analyze-room-photo.d.ts +29 -0
  238. package/dist/tools/vision/analyze-room-photo.d.ts.map +1 -0
  239. package/dist/tools/vision/analyze-room-photo.js +130 -0
  240. package/dist/tools/vision/index.d.ts +12 -0
  241. package/dist/tools/vision/index.d.ts.map +1 -0
  242. package/dist/tools/vision/index.js +14 -0
  243. package/dist/transports/http.d.ts +36 -0
  244. package/dist/transports/http.d.ts.map +1 -0
  245. package/dist/transports/http.js +203 -0
  246. package/dist/transports/stdio.d.ts +14 -0
  247. package/dist/transports/stdio.d.ts.map +1 -0
  248. package/dist/transports/stdio.js +16 -0
  249. package/package.json +86 -0
@@ -0,0 +1,146 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+ /**
4
+ * Input shape for `analyze_floorplan_image`.
5
+ *
6
+ * `image` is either a base64-encoded payload (optionally prefixed with a
7
+ * `data:image/<mime>;base64,` URL) or an `http(s)` URL which we fetch and
8
+ * inline as base64 before forwarding to the MCP host via sampling.
9
+ */
10
+ export const analyzeFloorplanImageInput = {
11
+ image: z.string().describe('Base64-encoded image or http(s) URL'),
12
+ scaleHint: z
13
+ .string()
14
+ .optional()
15
+ .describe("Text hint about scale, e.g. '1 cm = 1 m' or 'approximately 80 m²'"),
16
+ };
17
+ export const analyzeFloorplanImageOutput = {
18
+ walls: z.array(z.object({
19
+ start: z.tuple([z.number(), z.number()]),
20
+ end: z.tuple([z.number(), z.number()]),
21
+ thickness: z.number().optional(),
22
+ })),
23
+ rooms: z.array(z.object({
24
+ name: z.string(),
25
+ polygon: z.array(z.tuple([z.number(), z.number()])),
26
+ approximateAreaSqM: z.number().optional(),
27
+ })),
28
+ approximateDimensions: z.object({
29
+ widthM: z.number(),
30
+ depthM: z.number(),
31
+ }),
32
+ confidence: z.number().min(0).max(1),
33
+ };
34
+ const OutputSchema = z.object(analyzeFloorplanImageOutput);
35
+ const SYSTEM_PROMPT = `You are a vision assistant that extracts structured floor-plan data from an image.
36
+ Your ONLY job: return a JSON object that exactly matches this schema — no prose, no markdown fences.
37
+
38
+ {
39
+ "walls": [{ "start": [x, z], "end": [x, z], "thickness": number? }, ...],
40
+ "rooms": [{ "name": string, "polygon": [[x,z], ...], "approximateAreaSqM": number? }, ...],
41
+ "approximateDimensions": { "widthM": number, "depthM": number },
42
+ "confidence": number 0..1
43
+ }
44
+
45
+ Coordinates are in metres. Origin can be the floor plan's centre or bottom-left — be consistent.
46
+ If the image is unclear, lower the confidence score but still produce your best attempt.
47
+ DO NOT wrap the JSON in markdown. DO NOT explain. Just output the raw JSON.`;
48
+ const DATA_URI_RE = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i;
49
+ /**
50
+ * Resolve the `image` input into a sampling-ready image block.
51
+ *
52
+ * - `http(s)://` URLs are fetched, base64-encoded, and the mime type sniffed
53
+ * from the `content-type` response header.
54
+ * - `data:image/*;base64,...` URIs are stripped of the prefix; mime type taken
55
+ * from the URI itself.
56
+ * - Otherwise we treat the string as raw base64 and default to `image/jpeg`.
57
+ */
58
+ async function resolveImageBlock(image) {
59
+ if (/^https?:\/\//i.test(image)) {
60
+ // SSRF-safe fetch: blocks loopback / private / link-local / metadata IPs,
61
+ // caps size at 20 MB, times out at 10s, validates each redirect hop.
62
+ const { safeFetch } = await import('../../lib/safe-fetch');
63
+ const res = await safeFetch(image, { accept: 'image/*' });
64
+ const data = res.buffer.toString('base64');
65
+ const mimeType = res.contentType ?? 'image/jpeg';
66
+ return { type: 'image', data, mimeType };
67
+ }
68
+ const dataUriMatch = image.match(DATA_URI_RE);
69
+ if (dataUriMatch) {
70
+ return {
71
+ type: 'image',
72
+ mimeType: dataUriMatch[1],
73
+ data: dataUriMatch[2],
74
+ };
75
+ }
76
+ return { type: 'image', mimeType: 'image/jpeg', data: image };
77
+ }
78
+ /** Collect all text content blocks returned by the sampling host into one string. */
79
+ function extractText(content) {
80
+ const blocks = Array.isArray(content) ? content : [content];
81
+ const texts = [];
82
+ for (const block of blocks) {
83
+ if (block && typeof block === 'object' && block.type === 'text') {
84
+ const t = block.text;
85
+ if (typeof t === 'string')
86
+ texts.push(t);
87
+ }
88
+ }
89
+ return texts.join('\n').trim();
90
+ }
91
+ export function registerAnalyzeFloorplanImage(server, _bridge) {
92
+ server.registerTool('analyze_floorplan_image', {
93
+ title: 'Analyze floor-plan image',
94
+ description: 'Defer to the MCP host (via sampling) to extract walls, rooms, and approximate dimensions from a floor-plan image. Requires host support for sampling.',
95
+ inputSchema: analyzeFloorplanImageInput,
96
+ outputSchema: analyzeFloorplanImageOutput,
97
+ }, async ({ image, scaleHint }) => {
98
+ const caps = server.server.getClientCapabilities();
99
+ if (!caps?.sampling) {
100
+ throw new McpError(ErrorCode.InvalidRequest, 'sampling_unavailable');
101
+ }
102
+ const imageBlock = await resolveImageBlock(image);
103
+ const instruction = scaleHint
104
+ ? `Analyze this floor plan. Scale hint: ${scaleHint}. Return ONLY the JSON described by the system prompt.`
105
+ : 'Analyze this floor plan. Return ONLY the JSON described by the system prompt.';
106
+ const response = await server.server.createMessage({
107
+ systemPrompt: SYSTEM_PROMPT,
108
+ temperature: 0,
109
+ maxTokens: 2000,
110
+ messages: [
111
+ {
112
+ role: 'user',
113
+ content: [imageBlock, { type: 'text', text: instruction }],
114
+ },
115
+ ],
116
+ });
117
+ const text = extractText(response.content);
118
+ if (!text) {
119
+ throw new McpError(ErrorCode.InternalError, 'sampling_response_unparseable', {
120
+ reason: 'no text content returned by host',
121
+ });
122
+ }
123
+ let parsed;
124
+ try {
125
+ parsed = JSON.parse(text);
126
+ }
127
+ catch (err) {
128
+ throw new McpError(ErrorCode.InternalError, 'sampling_response_unparseable', {
129
+ raw: text,
130
+ reason: err instanceof Error ? err.message : String(err),
131
+ });
132
+ }
133
+ const validation = OutputSchema.safeParse(parsed);
134
+ if (!validation.success) {
135
+ throw new McpError(ErrorCode.InternalError, 'sampling_response_invalid', {
136
+ raw: text,
137
+ errors: validation.error.issues,
138
+ });
139
+ }
140
+ const payload = validation.data;
141
+ return {
142
+ content: [{ type: 'text', text: JSON.stringify(payload) }],
143
+ structuredContent: payload,
144
+ };
145
+ });
146
+ }
@@ -0,0 +1,29 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { SceneOperations } from '../../operations';
4
+ /**
5
+ * Input shape for `analyze_room_photo`.
6
+ *
7
+ * Same image resolution rules as `analyze_floorplan_image`.
8
+ */
9
+ export declare const analyzeRoomPhotoInput: {
10
+ image: z.ZodString;
11
+ };
12
+ export declare const analyzeRoomPhotoOutput: {
13
+ approximateDimensions: z.ZodObject<{
14
+ widthM: z.ZodNumber;
15
+ lengthM: z.ZodNumber;
16
+ heightM: z.ZodOptional<z.ZodNumber>;
17
+ }, z.core.$strip>;
18
+ identifiedFixtures: z.ZodArray<z.ZodObject<{
19
+ type: z.ZodString;
20
+ approximatePosition: z.ZodOptional<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>>;
21
+ }, z.core.$strip>>;
22
+ identifiedWindows: z.ZodArray<z.ZodObject<{
23
+ wallLabel: z.ZodOptional<z.ZodString>;
24
+ approximateWidthM: z.ZodOptional<z.ZodNumber>;
25
+ approximateHeightM: z.ZodOptional<z.ZodNumber>;
26
+ }, z.core.$strip>>;
27
+ };
28
+ export declare function registerAnalyzeRoomPhoto(server: McpServer, _bridge: SceneOperations): void;
29
+ //# sourceMappingURL=analyze-room-photo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyze-room-photo.d.ts","sourceRoot":"","sources":["../../../src/tools/vision/analyze-room-photo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAEvD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB;;CAEjC,CAAA;AAED,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;CAmBlC,CAAA;AAoED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAoE1F"}
@@ -0,0 +1,130 @@
1
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+ /**
4
+ * Input shape for `analyze_room_photo`.
5
+ *
6
+ * Same image resolution rules as `analyze_floorplan_image`.
7
+ */
8
+ export const analyzeRoomPhotoInput = {
9
+ image: z.string().describe('Base64-encoded image or http(s) URL'),
10
+ };
11
+ export const analyzeRoomPhotoOutput = {
12
+ approximateDimensions: z.object({
13
+ widthM: z.number(),
14
+ lengthM: z.number(),
15
+ heightM: z.number().optional(),
16
+ }),
17
+ identifiedFixtures: z.array(z.object({
18
+ type: z.string(),
19
+ approximatePosition: z.tuple([z.number(), z.number()]).optional(),
20
+ })),
21
+ identifiedWindows: z.array(z.object({
22
+ wallLabel: z.string().optional(),
23
+ approximateWidthM: z.number().optional(),
24
+ approximateHeightM: z.number().optional(),
25
+ })),
26
+ };
27
+ const OutputSchema = z.object(analyzeRoomPhotoOutput);
28
+ const SYSTEM_PROMPT = `You are a vision assistant that extracts structured room data from a single photograph.
29
+ Your ONLY job: return a JSON object that exactly matches this schema — no prose, no markdown fences.
30
+
31
+ {
32
+ "approximateDimensions": { "widthM": number, "lengthM": number, "heightM": number? },
33
+ "identifiedFixtures": [{ "type": string, "approximatePosition": [x, z]? }, ...],
34
+ "identifiedWindows": [{ "wallLabel": string?, "approximateWidthM": number?, "approximateHeightM": number? }, ...]
35
+ }
36
+
37
+ All measurements are in metres. "type" for fixtures is a short noun phrase such as "sofa", "kitchen island", "door".
38
+ If measurements cannot be estimated confidently, omit the optional fields rather than guessing.
39
+ DO NOT wrap the JSON in markdown. DO NOT explain. Just output the raw JSON.`;
40
+ const DATA_URI_RE = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i;
41
+ async function resolveImageBlock(image) {
42
+ if (/^https?:\/\//i.test(image)) {
43
+ // SSRF-safe fetch (see packages/mcp/src/lib/safe-fetch.ts).
44
+ const { safeFetch } = await import('../../lib/safe-fetch');
45
+ const res = await safeFetch(image, { accept: 'image/*' });
46
+ const data = res.buffer.toString('base64');
47
+ const mimeType = res.contentType ?? 'image/jpeg';
48
+ return { type: 'image', data, mimeType };
49
+ }
50
+ const dataUriMatch = image.match(DATA_URI_RE);
51
+ if (dataUriMatch) {
52
+ return {
53
+ type: 'image',
54
+ mimeType: dataUriMatch[1],
55
+ data: dataUriMatch[2],
56
+ };
57
+ }
58
+ return { type: 'image', mimeType: 'image/jpeg', data: image };
59
+ }
60
+ function extractText(content) {
61
+ const blocks = Array.isArray(content) ? content : [content];
62
+ const texts = [];
63
+ for (const block of blocks) {
64
+ if (block && typeof block === 'object' && block.type === 'text') {
65
+ const t = block.text;
66
+ if (typeof t === 'string')
67
+ texts.push(t);
68
+ }
69
+ }
70
+ return texts.join('\n').trim();
71
+ }
72
+ export function registerAnalyzeRoomPhoto(server, _bridge) {
73
+ server.registerTool('analyze_room_photo', {
74
+ title: 'Analyze room photo',
75
+ description: 'Defer to the MCP host (via sampling) to extract approximate dimensions, fixtures, and windows from a single-room photograph. Requires host support for sampling.',
76
+ inputSchema: analyzeRoomPhotoInput,
77
+ outputSchema: analyzeRoomPhotoOutput,
78
+ }, async ({ image }) => {
79
+ const caps = server.server.getClientCapabilities();
80
+ if (!caps?.sampling) {
81
+ throw new McpError(ErrorCode.InvalidRequest, 'sampling_unavailable');
82
+ }
83
+ const imageBlock = await resolveImageBlock(image);
84
+ const response = await server.server.createMessage({
85
+ systemPrompt: SYSTEM_PROMPT,
86
+ temperature: 0,
87
+ maxTokens: 2000,
88
+ messages: [
89
+ {
90
+ role: 'user',
91
+ content: [
92
+ imageBlock,
93
+ {
94
+ type: 'text',
95
+ text: 'Analyze this room photo. Return ONLY the JSON described by the system prompt.',
96
+ },
97
+ ],
98
+ },
99
+ ],
100
+ });
101
+ const text = extractText(response.content);
102
+ if (!text) {
103
+ throw new McpError(ErrorCode.InternalError, 'sampling_response_unparseable', {
104
+ reason: 'no text content returned by host',
105
+ });
106
+ }
107
+ let parsed;
108
+ try {
109
+ parsed = JSON.parse(text);
110
+ }
111
+ catch (err) {
112
+ throw new McpError(ErrorCode.InternalError, 'sampling_response_unparseable', {
113
+ raw: text,
114
+ reason: err instanceof Error ? err.message : String(err),
115
+ });
116
+ }
117
+ const validation = OutputSchema.safeParse(parsed);
118
+ if (!validation.success) {
119
+ throw new McpError(ErrorCode.InternalError, 'sampling_response_invalid', {
120
+ raw: text,
121
+ errors: validation.error.issues,
122
+ });
123
+ }
124
+ const payload = validation.data;
125
+ return {
126
+ content: [{ type: 'text', text: JSON.stringify(payload) }],
127
+ structuredContent: payload,
128
+ };
129
+ });
130
+ }
@@ -0,0 +1,12 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { SceneOperations } from '../../operations';
3
+ /**
4
+ * Register the vision-input tools that defer to the MCP host's sampling
5
+ * capability. No vision model is bundled in this package — if the host does
6
+ * not advertise `sampling` support, calling either tool returns
7
+ * `sampling_unavailable`.
8
+ */
9
+ export declare function registerVisionTools(server: McpServer, operations: SceneOperations): void;
10
+ export { analyzeFloorplanImageInput, analyzeFloorplanImageOutput, registerAnalyzeFloorplanImage, } from './analyze-floorplan-image';
11
+ export { analyzeRoomPhotoInput, analyzeRoomPhotoOutput, registerAnalyzeRoomPhoto, } from './analyze-room-photo';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tools/vision/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAIvD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,GAAG,IAAI,CAGxF;AAED,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAC3B,6BAA6B,GAC9B,MAAM,2BAA2B,CAAA;AAClC,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,sBAAsB,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { registerAnalyzeFloorplanImage } from './analyze-floorplan-image';
2
+ import { registerAnalyzeRoomPhoto } from './analyze-room-photo';
3
+ /**
4
+ * Register the vision-input tools that defer to the MCP host's sampling
5
+ * capability. No vision model is bundled in this package — if the host does
6
+ * not advertise `sampling` support, calling either tool returns
7
+ * `sampling_unavailable`.
8
+ */
9
+ export function registerVisionTools(server, operations) {
10
+ registerAnalyzeFloorplanImage(server, operations);
11
+ registerAnalyzeRoomPhoto(server, operations);
12
+ }
13
+ export { analyzeFloorplanImageInput, analyzeFloorplanImageOutput, registerAnalyzeFloorplanImage, } from './analyze-floorplan-image';
14
+ export { analyzeRoomPhotoInput, analyzeRoomPhotoOutput, registerAnalyzeRoomPhoto, } from './analyze-room-photo';
@@ -0,0 +1,36 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export type HttpTransportHandle = {
3
+ /** Host interface the server is listening on. */
4
+ host: string;
5
+ /** Port the server is actually listening on (useful when caller passed 0). */
6
+ port: number;
7
+ /** Gracefully close the HTTP server and the MCP transport. */
8
+ close(): Promise<void>;
9
+ };
10
+ export type HttpTransportOptions = {
11
+ /**
12
+ * Network interface to bind. Defaults to loopback. Binding to a non-loopback
13
+ * interface requires an auth token.
14
+ */
15
+ host?: string;
16
+ /** Bearer token for HTTP MCP calls. Defaults to PASCAL_MCP_HTTP_TOKEN. */
17
+ authToken?: string;
18
+ /** Exact CORS origins allowed to call this transport. Loopback origins are allowed. */
19
+ allowedOrigins?: string[];
20
+ /** Per-client request cap per minute. Set <= 0 to disable. */
21
+ rateLimitPerMinute?: number;
22
+ };
23
+ /**
24
+ * Attach an `McpServer` to a Streamable HTTP transport bound to a local port.
25
+ *
26
+ * Uses the SDK's Node-flavored `StreamableHTTPServerTransport`, which accepts
27
+ * `IncomingMessage`/`ServerResponse` directly via `handleRequest(req, res)`.
28
+ * A new session ID is generated per connection (stateful mode).
29
+ *
30
+ * Listens on `127.0.0.1:<port>` (pass `0` for an ephemeral port in tests). The
31
+ * returned handle exposes the actual bound port and a `close()` that stops
32
+ * the underlying Node HTTP server. To bind a public interface, pass `host` and
33
+ * configure an auth token.
34
+ */
35
+ export declare function connectHttp(server: McpServer, port: number, options?: HttpTransportOptions): Promise<HttpTransportHandle>;
36
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAUxE,MAAM,MAAM,mBAAmB,GAAG;IAChC,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAA;IACZ,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uFAAuF;IACvF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;IACzB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAgE9B"}
@@ -0,0 +1,203 @@
1
+ import { randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
+ const DEFAULT_HOST = '127.0.0.1';
5
+ const DEFAULT_RATE_LIMIT_PER_MINUTE = 120;
6
+ const WINDOW_MS = 60_000;
7
+ const ALLOWED_METHODS = 'GET, POST, DELETE, OPTIONS';
8
+ const ALLOWED_HEADERS = 'authorization, content-type, mcp-session-id, mcp-protocol-version, x-pascal-mcp-token';
9
+ /**
10
+ * Attach an `McpServer` to a Streamable HTTP transport bound to a local port.
11
+ *
12
+ * Uses the SDK's Node-flavored `StreamableHTTPServerTransport`, which accepts
13
+ * `IncomingMessage`/`ServerResponse` directly via `handleRequest(req, res)`.
14
+ * A new session ID is generated per connection (stateful mode).
15
+ *
16
+ * Listens on `127.0.0.1:<port>` (pass `0` for an ephemeral port in tests). The
17
+ * returned handle exposes the actual bound port and a `close()` that stops
18
+ * the underlying Node HTTP server. To bind a public interface, pass `host` and
19
+ * configure an auth token.
20
+ */
21
+ export async function connectHttp(server, port, options = {}) {
22
+ const host = options.host ?? DEFAULT_HOST;
23
+ const authToken = options.authToken ?? process.env.PASCAL_MCP_HTTP_TOKEN;
24
+ if (!(isLoopbackHost(host) || authToken)) {
25
+ throw new Error('HTTP transport on a non-loopback host requires PASCAL_MCP_HTTP_TOKEN or authToken');
26
+ }
27
+ const guard = createHttpGuard({
28
+ authToken,
29
+ allowedOrigins: options.allowedOrigins ?? envAllowedOrigins(),
30
+ rateLimitPerMinute: options.rateLimitPerMinute ?? DEFAULT_RATE_LIMIT_PER_MINUTE,
31
+ });
32
+ const transport = new StreamableHTTPServerTransport({
33
+ sessionIdGenerator: () => randomUUID(),
34
+ });
35
+ await server.connect(transport);
36
+ const httpServer = createServer((req, res) => {
37
+ if (!guard(req, res))
38
+ return;
39
+ transport.handleRequest(req, res).catch((err) => {
40
+ // Log to stderr; never touch stdout (stdio transport uses it).
41
+ console.error('[pascal-mcp] http transport error', err);
42
+ if (!res.writableEnded) {
43
+ try {
44
+ res.writeHead(500).end();
45
+ }
46
+ catch {
47
+ // Response may already be partially sent; nothing more we can do.
48
+ }
49
+ }
50
+ });
51
+ });
52
+ await new Promise((resolve, reject) => {
53
+ const onError = (err) => {
54
+ httpServer.off('listening', onListening);
55
+ reject(err);
56
+ };
57
+ const onListening = () => {
58
+ httpServer.off('error', onError);
59
+ resolve();
60
+ };
61
+ httpServer.once('error', onError);
62
+ httpServer.once('listening', onListening);
63
+ httpServer.listen(port, host);
64
+ });
65
+ const address = httpServer.address();
66
+ const boundPort = typeof address === 'object' && address !== null ? address.port : port;
67
+ return {
68
+ host,
69
+ port: boundPort,
70
+ close: async () => {
71
+ await new Promise((resolve, reject) => {
72
+ httpServer.close((err) => {
73
+ if (err)
74
+ reject(err);
75
+ else
76
+ resolve();
77
+ });
78
+ });
79
+ await transport.close();
80
+ },
81
+ };
82
+ }
83
+ function createHttpGuard(options) {
84
+ const buckets = new Map();
85
+ const allowedOrigins = new Set(options.allowedOrigins
86
+ .map(normalizeOrigin)
87
+ .filter((origin) => origin !== null));
88
+ return (req, res) => {
89
+ const origin = req.headers.origin;
90
+ if (origin && !isOriginAllowed(origin, req.headers.host, allowedOrigins)) {
91
+ sendJson(res, 403, { error: 'origin_not_allowed' });
92
+ return false;
93
+ }
94
+ applyCors(req, res, allowedOrigins);
95
+ if (req.method === 'OPTIONS') {
96
+ res.writeHead(204).end();
97
+ return false;
98
+ }
99
+ const pathname = req.url ? new URL(req.url, 'http://localhost').pathname : '/';
100
+ if (pathname !== '/mcp') {
101
+ sendJson(res, 404, { error: 'not_found' });
102
+ return false;
103
+ }
104
+ if (options.authToken) {
105
+ const supplied = bearerToken(req) ?? headerValue(req.headers['x-pascal-mcp-token']);
106
+ if (!(supplied && safeEqual(supplied, options.authToken))) {
107
+ sendJson(res, 401, { error: 'unauthorized' });
108
+ return false;
109
+ }
110
+ }
111
+ if (options.rateLimitPerMinute > 0) {
112
+ const now = Date.now();
113
+ const key = req.socket.remoteAddress ?? 'unknown';
114
+ const bucket = buckets.get(key);
115
+ if (!bucket || bucket.resetAt <= now) {
116
+ buckets.set(key, { count: 1, resetAt: now + WINDOW_MS });
117
+ }
118
+ else {
119
+ bucket.count++;
120
+ if (bucket.count > options.rateLimitPerMinute) {
121
+ res.setHeader('Retry-After', Math.max(1, Math.ceil((bucket.resetAt - now) / 1000)));
122
+ sendJson(res, 429, { error: 'rate_limited' });
123
+ return false;
124
+ }
125
+ }
126
+ }
127
+ return true;
128
+ };
129
+ }
130
+ function applyCors(req, res, allowedOrigins) {
131
+ const origin = req.headers.origin;
132
+ if (origin && isOriginAllowed(origin, req.headers.host, allowedOrigins)) {
133
+ res.setHeader('Access-Control-Allow-Origin', origin);
134
+ res.setHeader('Vary', 'Origin');
135
+ }
136
+ res.setHeader('Access-Control-Allow-Methods', ALLOWED_METHODS);
137
+ res.setHeader('Access-Control-Allow-Headers', ALLOWED_HEADERS);
138
+ res.setHeader('X-Content-Type-Options', 'nosniff');
139
+ }
140
+ function isOriginAllowed(origin, requestHost, allowedOrigins) {
141
+ const normalized = normalizeOrigin(origin);
142
+ if (!normalized)
143
+ return false;
144
+ const parsed = new URL(normalized);
145
+ if (isLoopbackHost(parsed.hostname))
146
+ return true;
147
+ if (requestHost && normalized === normalizeOrigin(`http://${requestHost}`))
148
+ return true;
149
+ if (requestHost && normalized === normalizeOrigin(`https://${requestHost}`))
150
+ return true;
151
+ return allowedOrigins.has(normalized);
152
+ }
153
+ function bearerToken(req) {
154
+ const header = headerValue(req.headers.authorization);
155
+ if (!header)
156
+ return null;
157
+ const match = header.match(/^Bearer\s+(.+)$/i);
158
+ return match?.[1] ?? null;
159
+ }
160
+ function headerValue(value) {
161
+ if (Array.isArray(value))
162
+ return value[0] ?? null;
163
+ return value ?? null;
164
+ }
165
+ function sendJson(res, status, payload) {
166
+ if (!res.hasHeader('Content-Type')) {
167
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
168
+ }
169
+ res.writeHead(status).end(JSON.stringify(payload));
170
+ }
171
+ function safeEqual(a, b) {
172
+ const aBuffer = Buffer.from(a);
173
+ const bBuffer = Buffer.from(b);
174
+ if (aBuffer.length !== bBuffer.length)
175
+ return false;
176
+ return timingSafeEqual(aBuffer, bBuffer);
177
+ }
178
+ function envAllowedOrigins() {
179
+ return (process.env.PASCAL_MCP_HTTP_ORIGINS ?? '')
180
+ .split(',')
181
+ .map((origin) => origin.trim())
182
+ .filter(Boolean);
183
+ }
184
+ function normalizeOrigin(origin) {
185
+ try {
186
+ const url = new URL(origin);
187
+ return `${url.protocol}//${url.host}`.toLowerCase();
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
193
+ function isLoopbackHost(host) {
194
+ const h = stripPort(host).toLowerCase();
195
+ return h === 'localhost' || h.endsWith('.localhost') || h === '127.0.0.1' || h === '::1';
196
+ }
197
+ function stripPort(host) {
198
+ if (host.startsWith('[')) {
199
+ const end = host.indexOf(']');
200
+ return end === -1 ? host : host.slice(1, end);
201
+ }
202
+ return host.split(':')[0] ?? host;
203
+ }
@@ -0,0 +1,14 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * Attach an `McpServer` to the stdio transport.
4
+ *
5
+ * The server takes ownership of stdin/stdout for JSON-RPC messaging. Callers
6
+ * must send any operator logging to stderr (not stdout) to avoid corrupting
7
+ * the protocol stream.
8
+ *
9
+ * Resolves once the transport is started. The underlying transport keeps the
10
+ * process alive as long as stdin is open, so callers typically just `await`
11
+ * this and then let the event loop run.
12
+ */
13
+ export declare function connectStdio(server: McpServer): Promise<void>;
14
+ //# sourceMappingURL=stdio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/transports/stdio.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAGxE;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAGnE"}
@@ -0,0 +1,16 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ /**
3
+ * Attach an `McpServer` to the stdio transport.
4
+ *
5
+ * The server takes ownership of stdin/stdout for JSON-RPC messaging. Callers
6
+ * must send any operator logging to stderr (not stdout) to avoid corrupting
7
+ * the protocol stream.
8
+ *
9
+ * Resolves once the transport is started. The underlying transport keeps the
10
+ * process alive as long as stdin is open, so callers typically just `await`
11
+ * this and then let the event loop run.
12
+ */
13
+ export async function connectStdio(server) {
14
+ const transport = new StdioServerTransport();
15
+ await server.connect(transport);
16
+ }