@pascal-app/mcp 1.0.6 → 1.0.7
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.
- package/README.md +31 -10
- package/dist/bin/pascal-mcp.js +2 -0
- package/dist/bridge/agent-session.d.ts +2 -1
- package/dist/bridge/agent-session.d.ts.map +1 -1
- package/dist/bridge/agent-session.js +0 -1
- package/dist/bridge/scene-bridge.d.ts +21 -10
- package/dist/bridge/scene-bridge.d.ts.map +1 -1
- package/dist/bridge/scene-bridge.js +178 -194
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/safe-fetch.d.ts +6 -0
- package/dist/lib/safe-fetch.d.ts.map +1 -1
- package/dist/lib/safe-fetch.js +135 -41
- package/dist/operations/scene-operations.d.ts +7 -0
- package/dist/operations/scene-operations.d.ts.map +1 -1
- package/dist/operations/scene-operations.js +4 -0
- package/dist/prompts/from-brief.js +1 -1
- package/dist/prompts/iterate-on-feedback.js +1 -1
- package/dist/prompts/renovation-from-photos.js +1 -1
- package/dist/prompts/scene-guidance.js +2 -2
- package/dist/resources/agent-guide.d.ts.map +1 -1
- package/dist/resources/agent-guide.js +8 -3
- package/dist/resources/catalog-items.d.ts +4 -1
- package/dist/resources/catalog-items.d.ts.map +1 -1
- package/dist/resources/catalog-items.js +14 -6
- package/dist/server.d.ts +9 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +21 -5
- package/dist/templates/empty-studio.d.ts.map +1 -1
- package/dist/templates/empty-studio.js +2 -3
- package/dist/templates/garden-house.d.ts.map +1 -1
- package/dist/templates/garden-house.js +2 -3
- package/dist/templates/two-bedroom.d.ts.map +1 -1
- package/dist/templates/two-bedroom.js +2 -3
- package/dist/tools/add-column.d.ts.map +1 -1
- package/dist/tools/add-column.js +9 -6
- package/dist/tools/add-object.d.ts +44 -5
- package/dist/tools/add-object.d.ts.map +1 -1
- package/dist/tools/add-object.js +75 -21
- package/dist/tools/asset-catalog.d.ts +5 -6
- package/dist/tools/asset-catalog.d.ts.map +1 -1
- package/dist/tools/asset-catalog.js +1 -9
- package/dist/tools/author-scene.d.ts +8 -5
- package/dist/tools/author-scene.d.ts.map +1 -1
- package/dist/tools/author-scene.js +302 -51
- package/dist/tools/clear-scene.js +2 -2
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +8 -5
- package/dist/tools/honest-patch-guard.js +6 -5
- package/dist/tools/hosted-services.d.ts.map +1 -1
- package/dist/tools/hosted-services.js +2 -2
- package/dist/tools/index.d.ts +7 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -9
- package/dist/tools/local-geometry-scripts.d.ts.map +1 -1
- package/dist/tools/local-geometry-scripts.js +7 -2
- package/dist/tools/patch-guards.d.ts.map +1 -1
- package/dist/tools/patch-guards.js +24 -14
- package/dist/tools/project-files.d.ts.map +1 -1
- package/dist/tools/project-files.js +2 -2
- package/dist/tools/room-tools.d.ts +1 -0
- package/dist/tools/room-tools.d.ts.map +1 -1
- package/dist/tools/room-tools.js +11 -8
- package/dist/tools/scene-scope.d.ts +4 -0
- package/dist/tools/scene-scope.d.ts.map +1 -0
- package/dist/tools/scene-scope.js +4 -0
- package/dist/tools/shared-tools.d.ts +0 -2
- package/dist/tools/shared-tools.d.ts.map +1 -1
- package/dist/tools/shared-tools.js +146 -31
- package/dist/tools/templates/create-house-from-brief.js +1 -1
- package/dist/tools/view-scene.js +2 -2
- package/examples/coordinate-conventions-demo.md +1 -0
- package/examples/photo-to-scene.md +1 -0
- package/examples/renovate-from-photos.md +2 -0
- package/package.json +10 -10
- package/dist/tools/create-unit.d.ts +0 -29
- package/dist/tools/create-unit.d.ts.map +0 -1
- package/dist/tools/create-unit.js +0 -36
- package/dist/tools/list-units.d.ts +0 -38
- package/dist/tools/list-units.d.ts.map +0 -1
- package/dist/tools/list-units.js +0 -61
- package/dist/tools/set-unit-members.d.ts +0 -22
- package/dist/tools/set-unit-members.d.ts.map +0 -1
- package/dist/tools/set-unit-members.js +0 -43
- package/dist/tools/unit-members.d.ts +0 -4
- package/dist/tools/unit-members.d.ts.map +0 -1
- package/dist/tools/unit-members.js +0 -24
package/dist/lib/safe-fetch.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
2
|
-
import { request as httpRequest } from 'node:http';
|
|
2
|
+
import { request as httpRequest, STATUS_CODES } from 'node:http';
|
|
3
3
|
import { request as httpsRequest } from 'node:https';
|
|
4
4
|
import { isIP } from 'node:net';
|
|
5
5
|
import { ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';
|
|
6
|
+
import { ALLOWED_ORIGINS_ENV, allowedOriginsFrom } from '@pascal-app/core/schema';
|
|
6
7
|
/**
|
|
7
8
|
* SSRF-safe fetch for user-supplied URLs (image URLs in vision tools).
|
|
8
9
|
*
|
|
9
10
|
* Every hop (the initial URL and each redirect target) is checked the same way:
|
|
10
|
-
* - only http(s) schemes
|
|
11
|
+
* - only http(s) schemes, on ports 80/443 unless the origin is allowlisted
|
|
11
12
|
* - well-known local hostnames are refused outright
|
|
12
13
|
* - the host is resolved once and *every* resolved address must be public;
|
|
13
14
|
* IPv6 forms that embed an IPv4 address (mapped, compatible, 6to4) are
|
|
@@ -15,6 +16,8 @@ import { ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';
|
|
|
15
16
|
* - the socket connects only to the addresses that were checked, so a DNS
|
|
16
17
|
* answer that changes between check and connect cannot reach a private host
|
|
17
18
|
* - optional origin allowlist via `PASCAL_ALLOWED_ASSET_ORIGINS` (comma-separated)
|
|
19
|
+
* - listed origins skip the port limit: an allowlisted origin may name any port
|
|
20
|
+
* - parsed by core's `allowedOriginsFrom`, the same reading as scene asset URLs
|
|
18
21
|
* - max body size (default 20 MB) and request timeout (default 10 s)
|
|
19
22
|
*
|
|
20
23
|
* Used by photo_to_scene / analyze_floorplan_image / analyze_room_photo.
|
|
@@ -22,6 +25,8 @@ import { ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';
|
|
|
22
25
|
const DEFAULT_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
|
|
23
26
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
24
27
|
const MAX_REDIRECTS = 3;
|
|
28
|
+
/** `URL.port` is '' when the URL uses its scheme's default port. */
|
|
29
|
+
const ALLOWED_PORTS = new Set(['', '80', '443']);
|
|
25
30
|
function parseIPv4(addr) {
|
|
26
31
|
const parts = addr.split('.');
|
|
27
32
|
if (parts.length !== 4 || parts.some((p) => !/^\d{1,3}$/.test(p)))
|
|
@@ -64,7 +69,11 @@ function parseIPv6(addr) {
|
|
|
64
69
|
return null;
|
|
65
70
|
return [...head, ...new Array(8 - explicit).fill(0), ...rest, ...tail];
|
|
66
71
|
}
|
|
67
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Ranges from the IANA IPv4 special-purpose registry that are not globally
|
|
74
|
+
* reachable, plus multicast. Globally reachable entries (AS112, AMT) stay allowed.
|
|
75
|
+
*/
|
|
76
|
+
function isBlockedV4([a, b, c]) {
|
|
68
77
|
if (a === 0)
|
|
69
78
|
return true; // 0.0.0.0/8 "this network"
|
|
70
79
|
if (a === 10)
|
|
@@ -77,12 +86,20 @@ function isBlockedV4([a, b]) {
|
|
|
77
86
|
return true; // 169.254.0.0/16 link-local (cloud metadata)
|
|
78
87
|
if (a === 172 && b >= 16 && b <= 31)
|
|
79
88
|
return true; // 172.16.0.0/12 private
|
|
80
|
-
if (a === 192 && b === 0)
|
|
89
|
+
if (a === 192 && b === 0 && c === 0)
|
|
81
90
|
return true; // 192.0.0.0/24 IETF protocol assignments
|
|
91
|
+
if (a === 192 && b === 0 && c === 2)
|
|
92
|
+
return true; // 192.0.2.0/24 TEST-NET-1
|
|
93
|
+
if (a === 192 && b === 88 && c === 99)
|
|
94
|
+
return true; // 192.88.99.0/24 deprecated 6to4 relay anycast
|
|
82
95
|
if (a === 192 && b === 168)
|
|
83
96
|
return true; // 192.168.0.0/16 private
|
|
84
97
|
if (a === 198 && (b === 18 || b === 19))
|
|
85
98
|
return true; // 198.18.0.0/15 benchmarking
|
|
99
|
+
if (a === 198 && b === 51 && c === 100)
|
|
100
|
+
return true; // 198.51.100.0/24 TEST-NET-2
|
|
101
|
+
if (a === 203 && b === 0 && c === 113)
|
|
102
|
+
return true; // 203.0.113.0/24 TEST-NET-3
|
|
86
103
|
if (a >= 224)
|
|
87
104
|
return true; // multicast, reserved, broadcast
|
|
88
105
|
return false;
|
|
@@ -100,12 +117,30 @@ function isBlockedV6(g) {
|
|
|
100
117
|
}
|
|
101
118
|
if (g0 === 0x64 && g1 === 0xff9b)
|
|
102
119
|
return true; // 64:ff9b::/96 and 64:ff9b:1::/48 NAT64
|
|
103
|
-
if (g0 === 0x100 && g1 === 0 && g2 === 0 && g3 === 0)
|
|
104
|
-
return true; // 100::/64 discard
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
if (g0 === 0x100 && g1 === 0 && g2 === 0 && (g3 === 0 || g3 === 1))
|
|
121
|
+
return true; // 100::/64 discard, 100:0:0:1::/64 dummy
|
|
122
|
+
// 2001::/23 IETF protocol assignments (incl. Teredo, benchmarking, ORCHID),
|
|
123
|
+
// except the entries IANA lists as globally reachable.
|
|
124
|
+
if (g0 === 0x2001 && g1 < 0x200) {
|
|
125
|
+
const anycast = g1 === 1 && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0 && g6 === 0;
|
|
126
|
+
if (anycast && g7 >= 1 && g7 <= 3)
|
|
127
|
+
return false; // 2001:1::1-3 PCP, TURN, DNS-SD SRP anycast
|
|
128
|
+
if (g1 === 3)
|
|
129
|
+
return false; // 2001:3::/32 AMT
|
|
130
|
+
if (g1 === 4 && g2 === 0x112)
|
|
131
|
+
return false; // 2001:4:112::/48 AS112-v6
|
|
132
|
+
if (g1 >= 0x20 && g1 <= 0x3f)
|
|
133
|
+
return false; // 2001:20::/28 ORCHIDv2, 2001:30::/28 drone remote ID
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (g0 === 0x2001 && g1 === 0xdb8)
|
|
137
|
+
return true; // 2001:db8::/32 documentation
|
|
107
138
|
if (g0 === 0x2002)
|
|
108
139
|
return isBlockedV4(embeddedV4(g1, g2)); // 2002::/16 6to4
|
|
140
|
+
if (g0 === 0x3fff && (g1 & 0xf000) === 0)
|
|
141
|
+
return true; // 3fff::/20 documentation
|
|
142
|
+
if (g0 === 0x5f00)
|
|
143
|
+
return true; // 5f00::/16 SRv6 SIDs
|
|
109
144
|
if ((g0 & 0xfe00) === 0xfc00)
|
|
110
145
|
return true; // fc00::/7 ULA (incl. fd00:ec2::254 metadata)
|
|
111
146
|
if ((g0 & 0xffc0) === 0xfe80)
|
|
@@ -139,40 +174,82 @@ function isLocalHostname(hostname) {
|
|
|
139
174
|
lower.endsWith('.internal') ||
|
|
140
175
|
lower.endsWith('.corp'));
|
|
141
176
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
177
|
+
const MAX_LOGGED_PATH = 128;
|
|
178
|
+
/**
|
|
179
|
+
* Scheme, host and a truncated path only: query strings can carry presigned
|
|
180
|
+
* tokens, userinfo carries credentials. Non-http(s) URLs (`data:`,
|
|
181
|
+
* `javascript:`) carry their payload in the path, so they log scheme and host.
|
|
182
|
+
*/
|
|
183
|
+
export function loggableUrl(url) {
|
|
184
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
185
|
+
return url.host ? `${url.protocol}//${url.host}` : url.protocol;
|
|
186
|
+
}
|
|
187
|
+
const path = url.pathname.length > MAX_LOGGED_PATH
|
|
188
|
+
? `${url.pathname.slice(0, MAX_LOGGED_PATH)}…`
|
|
189
|
+
: url.pathname;
|
|
190
|
+
return `${url.protocol}//${url.host}${path}`;
|
|
191
|
+
}
|
|
192
|
+
function blocked(reason, url, details = {}, log = {}) {
|
|
193
|
+
console.error(`[pascal-mcp] safe_fetch_blocked ${JSON.stringify({ reason, url: loggableUrl(url), ...details, ...log })}`);
|
|
194
|
+
return protocolError(reason, { url: loggableUrl(url), ...details });
|
|
145
195
|
}
|
|
146
196
|
function protocolError(reason, details) {
|
|
147
197
|
throw new ProtocolError(ProtocolErrorCode.InvalidParams, reason, details);
|
|
148
198
|
}
|
|
199
|
+
/** A system error's code (`ENOTFOUND`, `ECONNRESET`), never its message, which can quote the URL. */
|
|
200
|
+
function errorCode(err) {
|
|
201
|
+
const code = err?.code;
|
|
202
|
+
return typeof code === 'string' && /^[A-Z0-9_]{1,64}$/.test(code) ? code : 'unknown';
|
|
203
|
+
}
|
|
149
204
|
function assertAllowedUrl(url) {
|
|
150
205
|
let parsed;
|
|
151
206
|
try {
|
|
152
207
|
parsed = new URL(url);
|
|
153
208
|
}
|
|
154
209
|
catch {
|
|
155
|
-
return protocolError('invalid_url', {
|
|
210
|
+
return protocolError('invalid_url', {});
|
|
156
211
|
}
|
|
157
212
|
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
|
158
|
-
blocked('url_scheme_not_allowed', {
|
|
213
|
+
blocked('url_scheme_not_allowed', parsed, { protocol: parsed.protocol });
|
|
159
214
|
}
|
|
160
|
-
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
blocked('url_origin_not_allowlisted', { url, origin: parsed.origin });
|
|
215
|
+
// Node would send `user:pass@` as a Basic `Authorization` header.
|
|
216
|
+
if (parsed.username || parsed.password)
|
|
217
|
+
blocked('url_credentials_not_allowed', parsed);
|
|
218
|
+
const allowlist = allowedOriginsFrom(process.env[ALLOWED_ORIGINS_ENV]);
|
|
219
|
+
if (allowlist) {
|
|
220
|
+
if (!allowlist.has(parsed.origin)) {
|
|
221
|
+
blocked('url_origin_not_allowlisted', parsed, { origin: parsed.origin });
|
|
168
222
|
}
|
|
169
223
|
}
|
|
224
|
+
else if (!ALLOWED_PORTS.has(parsed.port)) {
|
|
225
|
+
// An operator-allowlisted origin may name any port; otherwise only web ports,
|
|
226
|
+
// so a public host cannot be used to probe its other services.
|
|
227
|
+
blocked('url_port_not_allowed', parsed, { port: parsed.port });
|
|
228
|
+
}
|
|
170
229
|
return parsed;
|
|
171
230
|
}
|
|
172
|
-
/**
|
|
173
|
-
|
|
231
|
+
/** Settles with `promise`, or rejects with the abort reason as soon as `signal` aborts. */
|
|
232
|
+
function untilAborted(promise, signal) {
|
|
233
|
+
if (signal.aborted)
|
|
234
|
+
return Promise.reject(signal.reason);
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
236
|
+
const onAbort = () => reject(signal.reason);
|
|
237
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
238
|
+
const settle = (done) => (arg) => {
|
|
239
|
+
signal.removeEventListener('abort', onAbort);
|
|
240
|
+
done(arg);
|
|
241
|
+
};
|
|
242
|
+
promise.then(settle(resolve), settle(reject));
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Resolves `url`'s host and returns its addresses only if every one of them is
|
|
247
|
+
* public. The lookup races `signal`: `dns.lookup` cannot be cancelled, so a
|
|
248
|
+
* hung resolver must not hold the fetch past its timeout.
|
|
249
|
+
*/
|
|
250
|
+
async function resolveCheckedAddresses(url, resolve, signal) {
|
|
174
251
|
const host = url.hostname.replace(/^\[|\]$/g, '');
|
|
175
|
-
const details = {
|
|
252
|
+
const details = { hostname: url.hostname };
|
|
176
253
|
const literalFamily = isIP(host);
|
|
177
254
|
let addresses;
|
|
178
255
|
if (literalFamily) {
|
|
@@ -180,22 +257,25 @@ async function resolveCheckedAddresses(url, resolve) {
|
|
|
180
257
|
}
|
|
181
258
|
else {
|
|
182
259
|
if (isLocalHostname(host))
|
|
183
|
-
blocked('url_host_blocked', details);
|
|
260
|
+
blocked('url_host_blocked', url, details);
|
|
184
261
|
try {
|
|
185
|
-
addresses = await resolve(host);
|
|
262
|
+
addresses = await untilAborted(resolve(host), signal);
|
|
186
263
|
}
|
|
187
264
|
catch (err) {
|
|
265
|
+
if (signal.aborted)
|
|
266
|
+
throw err;
|
|
188
267
|
return protocolError('dns_lookup_failed', {
|
|
268
|
+
url: loggableUrl(url),
|
|
189
269
|
...details,
|
|
190
|
-
|
|
270
|
+
code: errorCode(err),
|
|
191
271
|
});
|
|
192
272
|
}
|
|
193
273
|
}
|
|
194
274
|
if (addresses.length === 0)
|
|
195
|
-
protocolError('dns_lookup_failed', details);
|
|
275
|
+
protocolError('dns_lookup_failed', { url: loggableUrl(url), ...details, code: 'no_addresses' });
|
|
196
276
|
const unsafe = addresses.find((a) => isBlockedAddress(a.address));
|
|
197
277
|
if (unsafe)
|
|
198
|
-
blocked('url_host_blocked', details, { address: unsafe.address });
|
|
278
|
+
blocked('url_host_blocked', url, details, { address: unsafe.address });
|
|
199
279
|
return addresses;
|
|
200
280
|
}
|
|
201
281
|
/** A socket `lookup` that only ever answers with the given, already-checked addresses. */
|
|
@@ -247,40 +327,45 @@ export async function safeFetch(urlStr, opts = {}, deps = defaultDeps) {
|
|
|
247
327
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
248
328
|
const hops = [];
|
|
249
329
|
let current = urlStr;
|
|
330
|
+
let inFlight;
|
|
250
331
|
const controller = new AbortController();
|
|
251
332
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
252
333
|
try {
|
|
253
334
|
for (let i = 0; i <= MAX_REDIRECTS; i++) {
|
|
254
335
|
const parsed = assertAllowedUrl(current);
|
|
336
|
+
inFlight = parsed;
|
|
255
337
|
hops.push(parsed.toString());
|
|
256
|
-
const addresses = await resolveCheckedAddresses(parsed, deps.resolve);
|
|
338
|
+
const addresses = await resolveCheckedAddresses(parsed, deps.resolve, controller.signal);
|
|
257
339
|
const res = await deps.request(parsed, {
|
|
258
340
|
headers: opts.accept ? { Accept: opts.accept } : {},
|
|
259
341
|
lookup: pinnedLookup(addresses),
|
|
260
342
|
signal: controller.signal,
|
|
261
343
|
});
|
|
262
344
|
const status = res.statusCode ?? 0;
|
|
345
|
+
// Redirect and error bodies are never read: destroying the response
|
|
346
|
+
// (no keep-alive, `agent: false`) bounds them at zero bytes.
|
|
263
347
|
if (status >= 300 && status < 400) {
|
|
264
|
-
res.
|
|
348
|
+
res.destroy();
|
|
265
349
|
const location = header(res, 'location');
|
|
266
350
|
if (!location) {
|
|
267
|
-
protocolError('redirect_without_location', { url: parsed
|
|
351
|
+
protocolError('redirect_without_location', { url: loggableUrl(parsed), status });
|
|
268
352
|
}
|
|
269
353
|
current = new URL(location, parsed).toString();
|
|
270
354
|
continue;
|
|
271
355
|
}
|
|
272
356
|
if (status < 200 || status >= 300) {
|
|
273
|
-
res.
|
|
357
|
+
res.destroy();
|
|
274
358
|
protocolError('fetch_failed', {
|
|
275
|
-
url: parsed
|
|
359
|
+
url: loggableUrl(parsed),
|
|
276
360
|
status,
|
|
277
|
-
|
|
361
|
+
// The standard text, never the server's reason phrase, which can echo the URL.
|
|
362
|
+
statusText: STATUS_CODES[status] ?? '',
|
|
278
363
|
});
|
|
279
364
|
}
|
|
280
365
|
const declared = Number(header(res, 'content-length') ?? Number.NaN);
|
|
281
366
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
282
367
|
res.destroy();
|
|
283
|
-
protocolError('response_too_large', { url: parsed
|
|
368
|
+
protocolError('response_too_large', { url: loggableUrl(parsed), declared, maxBytes });
|
|
284
369
|
}
|
|
285
370
|
// Running cap so servers that lie about length still get bounded.
|
|
286
371
|
const chunks = [];
|
|
@@ -290,7 +375,11 @@ export async function safeFetch(urlStr, opts = {}, deps = defaultDeps) {
|
|
|
290
375
|
total += buf.byteLength;
|
|
291
376
|
if (total > maxBytes) {
|
|
292
377
|
res.destroy();
|
|
293
|
-
protocolError('response_too_large', {
|
|
378
|
+
protocolError('response_too_large', {
|
|
379
|
+
url: loggableUrl(parsed),
|
|
380
|
+
received: total,
|
|
381
|
+
maxBytes,
|
|
382
|
+
});
|
|
294
383
|
}
|
|
295
384
|
chunks.push(buf);
|
|
296
385
|
}
|
|
@@ -301,17 +390,22 @@ export async function safeFetch(urlStr, opts = {}, deps = defaultDeps) {
|
|
|
301
390
|
hops,
|
|
302
391
|
};
|
|
303
392
|
}
|
|
304
|
-
return protocolError('too_many_redirects', {
|
|
393
|
+
return protocolError('too_many_redirects', {
|
|
394
|
+
hops: hops.slice(0, MAX_REDIRECTS + 1).map((hop) => loggableUrl(new URL(hop))),
|
|
395
|
+
});
|
|
305
396
|
}
|
|
306
397
|
catch (err) {
|
|
307
398
|
if (err instanceof ProtocolError)
|
|
308
399
|
throw err;
|
|
309
400
|
if (controller.signal.aborted || err.name === 'AbortError') {
|
|
310
|
-
return protocolError('fetch_timeout', {
|
|
401
|
+
return protocolError('fetch_timeout', {
|
|
402
|
+
...(inFlight ? { url: loggableUrl(inFlight) } : {}),
|
|
403
|
+
timeoutMs,
|
|
404
|
+
});
|
|
311
405
|
}
|
|
312
406
|
return protocolError('fetch_error', {
|
|
313
|
-
url:
|
|
314
|
-
|
|
407
|
+
...(inFlight ? { url: loggableUrl(inFlight) } : {}),
|
|
408
|
+
code: errorCode(err),
|
|
315
409
|
});
|
|
316
410
|
}
|
|
317
411
|
finally {
|
|
@@ -91,6 +91,13 @@ export interface SceneOperations {
|
|
|
91
91
|
};
|
|
92
92
|
clearHistory(): void;
|
|
93
93
|
runAsSingleHistoryStep<T>(run: () => T): T;
|
|
94
|
+
/**
|
|
95
|
+
* Runs `run` synchronously in the scene's scope: its ids, history environment and disposal guard
|
|
96
|
+
* (`SceneBridge` over a runtime). Optional: an implementation without it runs `run` as is, which
|
|
97
|
+
* is right for a scene with random ids. One that computes on a detached scene must provide it,
|
|
98
|
+
* or the ids its tools mint are not the scene's.
|
|
99
|
+
*/
|
|
100
|
+
runInScene?<T>(run: () => T): T;
|
|
94
101
|
createProject(options: ProjectCreateOptions): Promise<ProjectStatus>;
|
|
95
102
|
getProjectStatus(id: string): Promise<ProjectStatus | null>;
|
|
96
103
|
saveScene(options: SceneSaveOptions): Promise<SceneMeta>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scene-operations.d.ts","sourceRoot":"","sources":["../../src/operations/scene-operations.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"scene-operations.d.ts","sourceRoot":"","sources":["../../src/operations/scene-operations.ts"],"names":[],"mappings":"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"}
|
|
@@ -155,6 +155,10 @@ class SceneOperationsFacade {
|
|
|
155
155
|
runAsSingleHistoryStep(run) {
|
|
156
156
|
return this.requireBridge().runAsSingleHistoryStep(run);
|
|
157
157
|
}
|
|
158
|
+
runInScene(run) {
|
|
159
|
+
// Store-only operations have no scene to scope: their tools (list_scenes) run as they are.
|
|
160
|
+
return this.#bridge ? this.#bridge.runInScene(run) : run();
|
|
161
|
+
}
|
|
158
162
|
async createProject(options) {
|
|
159
163
|
const store = this.requireStore();
|
|
160
164
|
if (!store.createProject) {
|
|
@@ -15,7 +15,7 @@ const PREAMBLE = [
|
|
|
15
15
|
' - Floor items live under a Level; wall/ceiling-attached items live under their target Wall or Ceiling. Do not place items directly under the Site node.',
|
|
16
16
|
'Use realistic dimensions in meters. Keep wall thickness small (0.1–0.3 m) and ceiling height 2.4–3.0 m unless the brief dictates otherwise.',
|
|
17
17
|
SCENE_DESIGN_GUIDANCE,
|
|
18
|
-
'
|
|
18
|
+
'When the work is done, report what `verify_scene` and `get_project_status` confirmed and the project `editorUrl`.',
|
|
19
19
|
].join('\n');
|
|
20
20
|
/**
|
|
21
21
|
* Build the user-facing prompt text for `from_brief`. Pure function for testability.
|
|
@@ -11,7 +11,7 @@ const PREAMBLE = [
|
|
|
11
11
|
' - Bundle related mutations into a single `apply_patch` call so they share one undo step.',
|
|
12
12
|
' - For multi-room changes, call verify_scene after the mutation and fix reported issues.',
|
|
13
13
|
SCENE_DESIGN_GUIDANCE,
|
|
14
|
-
|
|
14
|
+
" - When the work is done, report what you checked (`verify_scene` reports the scene's issues) and the project `editorUrl`, which `get_project_status` returns.",
|
|
15
15
|
].join('\n');
|
|
16
16
|
/**
|
|
17
17
|
* Build the user-facing prompt text for `iterate_on_feedback`.
|
|
@@ -10,7 +10,7 @@ const PREAMBLE = [
|
|
|
10
10
|
'Rules:',
|
|
11
11
|
' - Do not invent dimensions. Pull them from the analysis tool results.',
|
|
12
12
|
' - Do not modify nodes that are unrelated to the goals.',
|
|
13
|
-
|
|
13
|
+
" - When the work is done, report what you checked (`verify_scene` reports the scene's issues) and the project `editorUrl`, which `get_project_status` returns.",
|
|
14
14
|
].join('\n');
|
|
15
15
|
function isDataUrl(s) {
|
|
16
16
|
return s.startsWith('data:');
|
|
@@ -3,14 +3,14 @@ export const SCENE_DESIGN_GUIDANCE = [
|
|
|
3
3
|
' - Use meters. X/Z are horizontal floor-plan axes; Y is vertical.',
|
|
4
4
|
' - Read `pascal://agent/guide` when you need construction rules; do not inspect repository code for ordinary scene editing.',
|
|
5
5
|
' - Door default: 0.9m wide by 2.1m high, floor-mounted.',
|
|
6
|
-
' - Window default: 1.5m wide by 1.5m high
|
|
6
|
+
' - Window default: 1.5m wide by 1.5m high, centred vertically on its wall unless sillHeight is given.',
|
|
7
7
|
' - For clear concrete requests, act instead of asking for clarification.',
|
|
8
8
|
' - Defaults are for technical values (thicknesses, clearances, heights).',
|
|
9
9
|
' - What a request states (sizes, counts, rooms, style, references, "catalog only", a single room) binds the work exactly: a request for one bedroom is one bedroom.',
|
|
10
10
|
' - What it leaves open is design left to the agent: the size and number of storeys, the massing and roofscape, the site and outdoor spaces, the furniture and decor, and the floor, wall and ceiling finishes and materials. An open brief is room for a building with presence (composed volumes, interesting wings, roofs at different heights, good proportions, a considered site, rooms furnished and finished with intent) that stays credible and buildable, rather than a standard catalogue plan.',
|
|
11
11
|
' - When a choice would change the project, the person can be asked.',
|
|
12
12
|
" - The sources to build a scene from stand side by side: the catalog (search_assets, place_items, furnish_room), procedural designs (place_design, validate_design), authored objects of the agent's own (add_object), native materials (paint). The catalog holds a limited set that varies by server: what a search returns is what exists.",
|
|
13
|
-
' - design_brief records the request read axis by axis (action, size, pieces, openings, style, materials, source, dressing: each bounded by the person’s own words or open with a design statement) and where the objects come from; place_items, furnish_room, place_in_room, add_object and place_design follow it, run_batch included (search_assets and view_assets do not). Source open opens the made pieces and
|
|
13
|
+
' - design_brief records the request read axis by axis (action, size, form, facade, pieces, openings, style, materials, source, dressing: each bounded by the person’s own words or open with a design statement) and where the objects come from; place_items, furnish_room, place_in_room, add_object and place_design follow it, run_batch included (search_assets and view_assets do not). Source open opens the made pieces and places technical equipment and plants from the catalog; catalog, brand and existing open the catalog.',
|
|
14
14
|
' - Naming a piece (a double bed) states its function; its form, size within reason and finish are design unless the request fixes them.',
|
|
15
15
|
' - For full homes/apartments, include realistic support spaces: kitchen, living/dining, bathrooms, hallway/entry, storage/laundry where appropriate.',
|
|
16
16
|
' - For multi-story buildings, create separate level-owned story shells. Do not stretch first-floor exterior walls to cover upper floors.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-guide.d.ts","sourceRoot":"","sources":["../../src/resources/agent-guide.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"agent-guide.d.ts","sourceRoot":"","sources":["../../src/resources/agent-guide.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,eAAO,MAAM,WAAW,QA6FZ,CAAA;AAEZ,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAwCpF"}
|
|
@@ -8,7 +8,7 @@ export const AGENT_GUIDE = [
|
|
|
8
8
|
'## Projects, Saving and Output',
|
|
9
9
|
'',
|
|
10
10
|
'- `create_project` makes a browser-visible project and binds it to the session; `save_scene` without an id saves to the bound project. `get_project_status` is the authoritative status of a project: `editorUrl`, `browserVisibleVersion`, `latestVersion`, `publishedVersion`, `nodeCount` and `graphHash`.',
|
|
11
|
-
'- `create_house_from_brief`
|
|
11
|
+
'- `create_house_from_brief` replaces the scene with a starter template picked from its bedroom count, rooms and landscaping (not from the brief’s words), saves it as an unpublished draft and returns the editor URL. `create_story_shell`, `create_room`, `add_door`, `add_window`, `furnish_room`, `create_roof` and `place_items` each make one part of a building with the numbers the call gives.',
|
|
12
12
|
'- Semantic tools update the browser-visible draft as they run. `save_scene` with `saveMode: "draft"` saves the draft, autosave-style; `saveMode: "checkpoint"` records a version, a meaningful milestone.',
|
|
13
13
|
'- `verify_scene` reports a scene’s issues (`hasIssues`) and inventory; `get_project_status.nodeCount` is above 0 for a non-empty design.',
|
|
14
14
|
'- `editorUrl` is the link that opens the project in the browser editor, and the one the person is handed: it comes from tool output, and a route inferred from the project id is not a substitute.',
|
|
@@ -36,6 +36,10 @@ export const AGENT_GUIDE = [
|
|
|
36
36
|
'## Scene Facts and Tools',
|
|
37
37
|
'',
|
|
38
38
|
'- `get_site` gives the lot, the buildable area inside its setbacks with its centre and its frontage and depth on the street, the footprint a house usually covers, the ground and whether the lot is only the new-project placeholder, in the level coordinates you draw in. `place_building` takes the outline of the plan about to be drawn, in the coordinates it will be drawn in (and the yards a site plan gives), so the house rises inside the lot, centred. `create_room` and `add_wall` take your coordinates as given and say when they cross the lot line; `verify_scene` reports a house past it.',
|
|
39
|
+
'- `get_walls.wallTypes` lists assembly preset and project type ids and labels. `add_wall.type` copies the assembly and its total thickness; `set_wall_type({ wallIds, type })` changes existing standard walls (null clears the assembly, keeping thickness). Unknown ids refuse `unknown_wall_type`. Each wall reads its known type as `{ id, label }`, or null.',
|
|
40
|
+
'- `add_column` with role post makes a framing post: postSize 4x4, 6x6 (default), or 8x8; postBase none (default), post-base (steel), or pier (concrete). Height in metres defaults to the beam above or ceiling plane, as the Post tile does.',
|
|
41
|
+
'- `add_wall.wallType` is standard (default) or railing. A railing defaults to the Build panel post-and-rail guard, height 0.9144 m, with style-specific members. railing accepts style, postSpacing, postSize (sets thickness), infillSize, infillGap, bottomClearance (metres) and cap. It takes no assembly or foundation. Windows refuse railing_window; skirting refuses railing_wall; doors are gaps. `get_walls` reports wallType.',
|
|
42
|
+
'- `get_zones` reports roomType room (when absent), porch, deck or patio; floor.drop in metres (0 when absent; explicit floor.elevation wins); porchFraming auto, postSize, postSpacing in metres, beamSize, postCount and beamCount of generated parts still present (null with no outdoor/framing intent); and decking boardSize, gap in metres, direction, including deck defaults. Covered porches frame by default; decks/patios only with auto true. auto false leaves framing to hand edits. `set_zone_intent.patch` uses the same names and units.',
|
|
39
43
|
'- A semantic tool performs the editor’s own operation for what it makes; `apply_patch` writes graph nodes as given, so it carries none of that.',
|
|
40
44
|
"- A call whose arguments a tool's schema rejects is refused with the code `invalid_input` before anything runs and its message names the arguments. A level you write without a `height` is given the height the editor would derive from what it holds (an empty level 2.5 m), and `apply_patch` says so in `notes`.",
|
|
41
45
|
'- `apply_patch` takes create/update/delete ops in one call, applied in order so later ops can reference ids created earlier. A single call is atomic (all or nothing).',
|
|
@@ -50,6 +54,7 @@ export const AGENT_GUIDE = [
|
|
|
50
54
|
'- Adjacent rooms can share one wall (or have openings that line up). A door keeps about 0.65 m clear on both sides; furniture footprints do not overlap.',
|
|
51
55
|
'- A complete home is an exterior shell, interior rooms, openings, a roof, furniture and landscaping.',
|
|
52
56
|
'- On doors and windows, `t` or `position` runs from 0 to 1 along the wall unless a tool explicitly says otherwise.',
|
|
57
|
+
'- `move_node` moves one node re-seated on its host, as the editor’s move tool does: a door, window or wall item slides along its wall (plan target projected onto the wall, clamped, overlap refused unless force), a ceiling item in plan under its ceiling, a floor node in level plan, a roof piece in its segment-local plan, a wall by translation only. `duplicate_node` copies nodes with their hosted children onto fresh ids, on the same hosts or another level for level-parented roots.',
|
|
53
58
|
'- X/Z are floor-plan axes and Y is vertical; dimensions are meters.',
|
|
54
59
|
"- `create_roof` roofs the storeys and exterior walls standing when it runs. A building's roof goes on its roof level, the level directly above the top storey (named Roof, made when there is none), so exploded and solo level views lift it off the plan; a roof level is not an occupied story. `add_level` puts a new storey under it. A storey takes one roof: `roof_exists` names the one there. A project built before the roof level holds its roof on the storey (`verify_scene`'s `roof_mixed_in_storey`): `move_roofs_to_roof_level` moves it there as it stands, without a delete and rebuild.",
|
|
55
60
|
'- Glazed roof construction supports gable, straight shed and barrel roofs. For barrel, depth is the span and barrelRise is the arch rise, at most half the span. glazing configures frame, glass, grids and panels. Paint glazing-frame, glazing-glass or glazing-solid; without a role, frame and solid panels are painted while glass keeps its finish.',
|
|
@@ -62,7 +67,7 @@ export const AGENT_GUIDE = [
|
|
|
62
67
|
'',
|
|
63
68
|
'## Looking and Starting Over',
|
|
64
69
|
'',
|
|
65
|
-
"- `view_scene` renders what was built, every storey and the roof, from a viewpoint you pick (a compass side, a side as a front photo names it, from the street the site records or else the house's own front, a height, a street-level eye, an orthographic face, a photo's own camera, in the photo's light). It is a picture to compare against a reference, not a measurement. `interior: { zoneId }` stands inside a room at eye height, a step in from a corner and looking across it. `interior: { levelId }` shows the rooms of one floor: the other floors, its roof and its ceilings are left out of the picture and everything on it, walls, openings and furniture alike, is cut at 0.65 of the storey's height.
|
|
70
|
+
"- `view_scene` renders what was built, every storey and the roof, from a viewpoint you pick (a compass side, a side as a front photo names it, from the street the site records or else the house's own front, a height, a street-level eye, an orthographic face, a photo's own camera, in the photo's light). It is a picture to compare against a reference, not a measurement. `interior: { zoneId }` stands inside a room at eye height, a step in from a corner and looking across it. `interior: { levelId }` shows the rooms of one floor: the other floors, its roof and its ceilings are left out of the picture and everything on it, walls, openings and furniture alike, is cut at 0.65 of the storey's height. The server has no renderer of its own: the host renders the view, through an editor tab open on the project or a renderer of its own. When nothing can render it, the call is refused with its reason (`view_unavailable` on a server with no rendering; a hosted server may answer with its own codes, such as `editor_tab_required`, `editor_tab_hidden` or `editor_tab_busy`, each saying what to do next): follow the refusal's own words, and take sizes and counts from the tools.",
|
|
66
71
|
'- `clear_scene` empties the project on purpose, for when the person asked to start over, keeping the site, the building and one level. Any other write that would leave the project empty is refused as `scene_wipe_blocked`, and nothing changes.',
|
|
67
72
|
'',
|
|
68
73
|
'## Custom Designs',
|
|
@@ -81,7 +86,7 @@ export const AGENT_GUIDE = [
|
|
|
81
86
|
'',
|
|
82
87
|
'- What a request states (sizes, counts, rooms, style, references, "catalog only", a single room) binds the work exactly: a request for one bedroom is one bedroom. What it leaves open is design left to the agent: the size and number of storeys, the massing and roofscape, the site and outdoor spaces, the furniture and decor, and the floor, wall and ceiling finishes and materials. Defaults are for technical values (thicknesses, clearances, heights). An open brief is room for a building with presence (composed volumes, interesting wings, roofs at different heights, good proportions, a considered site, rooms furnished and finished with intent) that stays credible and buildable, rather than a standard catalogue plan. When a choice would change the project, the person can be asked.',
|
|
83
88
|
'- The sources to build a scene from stand side by side: the catalog (`search_assets`, `place_items`, `furnish_room`), procedural designs (`place_design`, `validate_design`), authored objects of the agent’s own (`add_object`), native materials (`paint`). The catalog holds a limited set that varies by server: what a search returns is what exists.',
|
|
84
|
-
'- Naming a piece (a double bed) states its function; its form, size within reason and finish are design unless the request fixes them. `design_brief` records the request as a brief, read axis by axis (action, size, pieces, openings, style, materials, source, dressing: each bounded by the person’s own words or open with a design statement; the source is `open`, `catalog`, `brand` or `existing`) and furnishings are placed or made after it, inside `run_batch` too: `search_assets` and `view_assets` read the catalog at any time. Source `open` opens `add_object` and `place_design`, and places
|
|
89
|
+
'- Naming a piece (a double bed) states its function; its form, size within reason and finish are design unless the request fixes them. `design_brief` records the request as a brief, read axis by axis (action, size, form, facade, pieces, openings, style, materials, source, dressing: each bounded by the person’s own words or open with a design statement; the source is `open`, `catalog`, `brand` or `existing`) and furnishings are placed or made after it, inside `run_batch` too: `search_assets` and `view_assets` read the catalog at any time. Source `open` opens `add_object` and `place_design`, and places technical equipment and plants from the catalog (the catalog’s function tags tell them from furniture); every other catalog piece is made, and `furnish_room` stays closed. The other sources open `place_items`, `furnish_room` and `place_in_room`, with creation for what the catalog lacks. A non-open source quotes the request in `sourceQuote`. `beyond` records eight dressing layers; an open `dressing` axis names pieces in at least six, and the result returns them as `dressing`. The brief is the session’s: a scene load starts another, and the next `design_brief` replaces it.',
|
|
85
90
|
'',
|
|
86
91
|
'## Output Contract',
|
|
87
92
|
'',
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
2
|
import type { SceneOperations } from '../operations/index.js';
|
|
3
3
|
import { type AssetCatalog } from '../tools/asset-catalog.js';
|
|
4
|
+
/** The items the resource lists of a library searched where it is kept; search_assets finds the rest. */
|
|
5
|
+
export declare const CATALOG_RESOURCE_LIMIT = 100;
|
|
4
6
|
/**
|
|
5
7
|
* `pascal://catalog/items` — the items place_items draws from: the host's library when it passes
|
|
6
|
-
* one, else a dependency-free built-in subset, so headless agents can still place furniture.
|
|
8
|
+
* one, else a dependency-free built-in subset, so headless agents can still place furniture. A
|
|
9
|
+
* library kept elsewhere is listed by its first items and its size, never whole.
|
|
7
10
|
*/
|
|
8
11
|
export declare function registerCatalogItems(server: McpServer, _bridge: SceneOperations, catalog?: AssetCatalog): void;
|
|
9
12
|
//# sourceMappingURL=catalog-items.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog-items.d.ts","sourceRoot":"","sources":["../../src/resources/catalog-items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;
|
|
1
|
+
{"version":3,"file":"catalog-items.d.ts","sourceRoot":"","sources":["../../src/resources/catalog-items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAE7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACpD,OAAO,EAAE,KAAK,YAAY,EAAkB,MAAM,wBAAwB,CAAA;AAE1E,yGAAyG;AACzG,eAAO,MAAM,sBAAsB,MAAM,CAAA;AAKzC;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,YAA6B,GACrC,IAAI,CA6BN"}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { isAssetCatalogSource } from '@pascal-app/core/agent-operations';
|
|
1
2
|
import { builtInCatalog } from '../tools/asset-catalog.js';
|
|
3
|
+
/** The items the resource lists of a library searched where it is kept; search_assets finds the rest. */
|
|
4
|
+
export const CATALOG_RESOURCE_LIMIT = 100;
|
|
5
|
+
const NOTE = 'A fixture this catalog lacks (a wall light, a house number) is built as a design: place_design.';
|
|
2
6
|
/**
|
|
3
7
|
* `pascal://catalog/items` — the items place_items draws from: the host's library when it passes
|
|
4
|
-
* one, else a dependency-free built-in subset, so headless agents can still place furniture.
|
|
8
|
+
* one, else a dependency-free built-in subset, so headless agents can still place furniture. A
|
|
9
|
+
* library kept elsewhere is listed by its first items and its size, never whole.
|
|
5
10
|
*/
|
|
6
11
|
export function registerCatalogItems(server, _bridge, catalog = builtInCatalog) {
|
|
7
12
|
server.registerResource('catalog-items', 'pascal://catalog/items', {
|
|
@@ -9,11 +14,14 @@ export function registerCatalogItems(server, _bridge, catalog = builtInCatalog)
|
|
|
9
14
|
description: "Placeable items: the host's library when it has one, else a built-in subset.",
|
|
10
15
|
mimeType: 'application/json',
|
|
11
16
|
}, async (uri) => {
|
|
12
|
-
const payload =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const payload = isAssetCatalogSource(catalog)
|
|
18
|
+
? await catalog.search({ limit: CATALOG_RESOURCE_LIMIT }).then(({ items, total }) => ({
|
|
19
|
+
status: 'ok',
|
|
20
|
+
items,
|
|
21
|
+
total,
|
|
22
|
+
note: `${items.length < total ? `The library holds ${total} items; these are the first ${items.length}, and search_assets finds any of them. ` : ''}${NOTE}`,
|
|
23
|
+
}))
|
|
24
|
+
: { status: 'ok', items: catalog, note: NOTE };
|
|
17
25
|
return {
|
|
18
26
|
contents: [
|
|
19
27
|
{
|
package/dist/server.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
-
import { type HostedServiceExecutor, type ProjectFileToolExecutor } from '@pascal-app/core/agent-operations';
|
|
2
|
+
import { type HostedServiceExecutor, type ProjectFileReader, type ProjectFileToolExecutor } from '@pascal-app/core/agent-operations';
|
|
3
3
|
import type { SceneBridge } from './bridge/scene-bridge.js';
|
|
4
4
|
import { type SceneOperations } from './operations/index.js';
|
|
5
5
|
import type { SceneStore } from './storage/types.js';
|
|
6
|
-
import type { GeometryScriptHost } from './tools/add-object.js';
|
|
7
|
-
import {
|
|
6
|
+
import type { GeometryScriptHost, ObjectMakerHost } from './tools/add-object.js';
|
|
7
|
+
import type { AssetCatalog } from './tools/asset-catalog.js';
|
|
8
8
|
import type { PlanReadingHosts } from './tools/shared-tools.js';
|
|
9
9
|
import type { SceneViewHost } from './tools/view-scene.js';
|
|
10
10
|
export type PascalMcpToolExecutor = <Result>(input: {
|
|
@@ -28,16 +28,20 @@ export type CreatePascalMcpServerOptions = {
|
|
|
28
28
|
*/
|
|
29
29
|
executeTool?: PascalMcpToolExecutor;
|
|
30
30
|
/**
|
|
31
|
-
* The items search_assets, place_items and furnish_room draw from,
|
|
32
|
-
* hosted app
|
|
31
|
+
* The items search_assets, place_items and furnish_room draw from: a list, or a source each call
|
|
32
|
+
* reads what it needs of (the hosted app's published library); without it, a small built-in list.
|
|
33
33
|
*/
|
|
34
34
|
catalog?: AssetCatalog;
|
|
35
35
|
/** Runs and stores `add_object` modules; without it the tool answers `scripts_unavailable`. */
|
|
36
36
|
geometryScripts?: GeometryScriptHost;
|
|
37
|
+
/** Makes `add_object`'s pieces from words with the host's models; without it they are refused. */
|
|
38
|
+
objectMakers?: ObjectMakerHost;
|
|
37
39
|
/** Optional authenticated hosted services; local scene tools remain usable without them. */
|
|
38
40
|
services?: HostedServiceExecutor;
|
|
39
41
|
/** Authenticated project file catalog; absent for a local scene without hosted files. */
|
|
40
42
|
files?: ProjectFileToolExecutor;
|
|
43
|
+
/** Reads a project file's bytes (author_scene's payloadFile); absent without hosted files. */
|
|
44
|
+
readFile?: ProjectFileReader;
|
|
41
45
|
/** Asks an editor open on the project for a picture (`view_scene`); without it, refused. */
|
|
42
46
|
sceneViews?: SceneViewHost;
|
|
43
47
|
/** Reads catalog items' pictures and surfaces before view_assets and paint. */
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EAKV,MAAM,8BAA8B,CAAA;AACrC,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,uBAAuB,EAG7B,MAAM,mCAAmC,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EAKV,MAAM,8BAA8B,CAAA;AACrC,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAG7B,MAAM,mCAAmC,CAAA;AAE1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAG1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAKzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAIvD,MAAM,MAAM,qBAAqB,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,WAAW,CAAA;IACnB,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;CAC/B,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;AAErB,MAAM,MAAM,4BAA4B,GAAG;IACzC,MAAM,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,eAAe,CAAA;IAC5B,mFAAmF;IACnF,KAAK,CAAC,EAAE,UAAU,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAA;IACnC;;;OAGG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,+FAA+F;IAC/F,eAAe,CAAC,EAAE,kBAAkB,CAAA;IACpC,kGAAkG;IAClG,YAAY,CAAC,EAAE,eAAe,CAAA;IAC9B,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,qBAAqB,CAAA;IAChC,yFAAyF;IACzF,KAAK,CAAC,EAAE,uBAAuB,CAAA;IAC/B,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,4FAA4F;IAC5F,UAAU,CAAC,EAAE,aAAa,CAAA;IAC1B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,gBAAgB,CAAC,YAAY,CAAC,CAAA;IAC3C,yFAAyF;IACzF,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B,GAAG,SAAS,CA8BnF"}
|