pi-tldraw 0.1.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/bridge/app-bridge-entry.js +6 -0
  4. package/mcp-app/LICENSE.md +9 -0
  5. package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
  6. package/mcp-app/README.md +129 -0
  7. package/mcp-app/dev-tunnel.sh +51 -0
  8. package/mcp-app/dist/editor-api.json +8493 -0
  9. package/mcp-app/dist/mcp-app.html +643 -0
  10. package/mcp-app/dist/method-map.json +915 -0
  11. package/mcp-app/package.json +42 -0
  12. package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
  13. package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
  14. package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
  15. package/mcp-app/scripts/extract-editor-api.ts +1374 -0
  16. package/mcp-app/server.json +21 -0
  17. package/mcp-app/src/logger.ts +45 -0
  18. package/mcp-app/src/register-tools.ts +368 -0
  19. package/mcp-app/src/shared/generated-data.ts +160 -0
  20. package/mcp-app/src/shared/pending-requests.ts +69 -0
  21. package/mcp-app/src/shared/types.ts +76 -0
  22. package/mcp-app/src/shared/utils.ts +132 -0
  23. package/mcp-app/src/tools/exec.ts +120 -0
  24. package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
  25. package/mcp-app/src/tools/search.ts +150 -0
  26. package/mcp-app/src/widget/app-context.tsx +29 -0
  27. package/mcp-app/src/widget/dev-log.tsx +70 -0
  28. package/mcp-app/src/widget/exec-helpers.ts +232 -0
  29. package/mcp-app/src/widget/export-tldr.ts +35 -0
  30. package/mcp-app/src/widget/focused/defaults.ts +141 -0
  31. package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
  32. package/mcp-app/src/widget/focused/format.ts +366 -0
  33. package/mcp-app/src/widget/focused/to-focused.ts +258 -0
  34. package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
  35. package/mcp-app/src/widget/image-guard.tsx +106 -0
  36. package/mcp-app/src/widget/index.html +33 -0
  37. package/mcp-app/src/widget/mcp-app.css +113 -0
  38. package/mcp-app/src/widget/mcp-app.tsx +857 -0
  39. package/mcp-app/src/widget/persistence.ts +337 -0
  40. package/mcp-app/src/widget/snapshot.ts +157 -0
  41. package/mcp-app/src/worker.ts +305 -0
  42. package/mcp-app/tsconfig.json +23 -0
  43. package/mcp-app/vite.config.ts +13 -0
  44. package/mcp-app/wrangler.toml +45 -0
  45. package/mcp-app-source.json +36 -0
  46. package/package.json +90 -0
  47. package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
  48. package/scripts/assemble-mcp-app.mjs +193 -0
  49. package/scripts/build-bridge.mjs +74 -0
  50. package/scripts/e2e-mcp.mjs +69 -0
  51. package/scripts/e2e-packaged-mcp-app.mjs +79 -0
  52. package/scripts/run-mcp-app-dev.mjs +44 -0
  53. package/scripts/verify-bundle.mjs +41 -0
  54. package/scripts/verify-mcp-app-source.mjs +51 -0
  55. package/scripts/verify-mcp-app.mjs +38 -0
  56. package/scripts/verify-package-files.mjs +50 -0
  57. package/src/canvas/export.ts +164 -0
  58. package/src/canvas/state.ts +117 -0
  59. package/src/canvas/workflow.ts +105 -0
  60. package/src/commands/tldraw-command.ts +48 -0
  61. package/src/diagram/guidance.ts +44 -0
  62. package/src/host/local-host.ts +289 -0
  63. package/src/index.ts +762 -0
  64. package/src/mcp/client.ts +126 -0
  65. package/src/mcp/response.ts +74 -0
  66. package/src/semantic/layer.ts +309 -0
  67. package/src/server/server-manager.ts +153 -0
  68. package/src/store/export-store.ts +33 -0
  69. package/src/store/project-store.ts +251 -0
  70. package/src/ui/tldraw-status.ts +88 -0
  71. package/static/app-bridge-bundle.js +18114 -0
  72. package/static/app-bridge-bundle.meta.json +164 -0
  73. package/static/host.html +390 -0
  74. package/tsconfig.json +13 -0
@@ -0,0 +1,305 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ /**
4
+ * Cloudflare Workers entry point for the tldraw MCP server.
5
+ *
6
+ * Uses a Durable Object (McpAgent) with SQLite for persistent checkpoint storage,
7
+ * R2 for image uploads, and the shared registerTools() for tool registration.
8
+ */
9
+
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
11
+ import { McpAgent } from 'agents/mcp'
12
+ import { Logger } from './logger'
13
+ import { registerTools } from './register-tools'
14
+ import { loadEditorApiSpecFromAssets, loadMethodMapFromAssets } from './shared/generated-data'
15
+ import { PendingRequests } from './shared/pending-requests'
16
+ import {
17
+ MAX_CHECKPOINTS,
18
+ MCP_SERVER_DESCRIPTION,
19
+ MCP_SERVER_INSTRUCTIONS,
20
+ MCP_SERVER_NAME,
21
+ MCP_SERVER_TITLE,
22
+ MCP_SERVER_VERSION,
23
+ MCP_SERVER_WEBSITE_URL,
24
+ } from './shared/types'
25
+ import type { MCP_APP_HOST_NAMES, PendingBootstrap, ServerDeps } from './shared/types'
26
+ import { resolveMcpAppHostNameFromServerInfo } from './shared/utils'
27
+
28
+ // --- Types ---
29
+
30
+ interface Env {
31
+ MCP_OBJECT: DurableObjectNamespace
32
+ ASSETS: Fetcher
33
+ LOADER: WorkerLoader
34
+ RATE_LIMITER: RateLimit
35
+ MCP_AUTH_TOKEN: string
36
+ MCP_IS_DEV: string
37
+ WORKER_ORIGIN: string
38
+ MCP_ANALYTICS?: AnalyticsEngineDataset
39
+ }
40
+
41
+ // --- Widget HTML loader ---
42
+
43
+ async function loadWidgetHtml(assets: Fetcher): Promise<string> {
44
+ const response = await assets.fetch(new Request('https://assets.local/mcp-app.html'))
45
+ if (!response.ok) throw new Error(`Failed to load widget HTML: ${response.status}`)
46
+ return response.text()
47
+ }
48
+
49
+ // --- CORS helpers ---
50
+
51
+ const CORS_HEADERS: Record<string, string> = {
52
+ 'Access-Control-Allow-Origin': '*',
53
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
54
+ 'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization, Mcp-Session-Id',
55
+ }
56
+
57
+ function corsResponse(response: Response): Response {
58
+ const headers = new Headers(response.headers)
59
+ for (const [key, value] of Object.entries(CORS_HEADERS)) {
60
+ headers.set(key, value)
61
+ }
62
+ return new Response(response.body, { status: response.status, headers })
63
+ }
64
+
65
+ // --- McpAgent Durable Object ---
66
+
67
+ export class TldrawMCP extends McpAgent<Env> {
68
+ override server = new McpServer(
69
+ {
70
+ name: MCP_SERVER_NAME,
71
+ title: MCP_SERVER_TITLE,
72
+ version: MCP_SERVER_VERSION,
73
+ description: MCP_SERVER_DESCRIPTION,
74
+ websiteUrl: MCP_SERVER_WEBSITE_URL,
75
+ },
76
+ {
77
+ instructions: MCP_SERVER_INSTRUCTIONS,
78
+ }
79
+ )
80
+ isDev = this.env.MCP_IS_DEV === 'true'
81
+ logsEnabled = this.isDev
82
+ activeCheckpointId: string | null = null
83
+ sessionId: string = ''
84
+ logger = new Logger('TldrawMCP', this.logsEnabled)
85
+ clientHostName: MCP_APP_HOST_NAMES | undefined = undefined
86
+ pendingRequests = new PendingRequests()
87
+ pendingBootstrap: PendingBootstrap | null = null
88
+
89
+ /** The MCP session ID used for DO routing (extracted from DO name). */
90
+ getMcpSessionId(): string {
91
+ return (this as any).name?.replace(/^streamable-http:/, '') ?? ''
92
+ }
93
+
94
+ async init() {
95
+ this.server.server.oninitialized = () => {
96
+ const clientInfo = this.server.server.getClientVersion()
97
+ const resolved = resolveMcpAppHostNameFromServerInfo(clientInfo?.name ?? '')
98
+ if (resolved) {
99
+ this.clientHostName = resolved
100
+ void this
101
+ .sql`INSERT OR REPLACE INTO meta (key, value) VALUES ('clientHostName', ${resolved})`
102
+ }
103
+ this.logger.info(`Client connected: ${this.clientHostName ?? 'unknown'}`)
104
+ }
105
+
106
+ // --- DO SQLite setup ---
107
+ void this
108
+ .sql`CREATE TABLE IF NOT EXISTS checkpoints (id TEXT PRIMARY KEY, data TEXT, created_at INTEGER)`
109
+ void this.sql`CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)`
110
+ void this
111
+ .sql`CREATE TABLE IF NOT EXISTS canvas_checkpoints (canvas_id TEXT PRIMARY KEY, checkpoint_id TEXT)`
112
+
113
+ // Restore active checkpoint on reconnect
114
+ const rows = [...this.sql`SELECT value FROM meta WHERE key = 'activeCheckpointId'`]
115
+ if (rows.length > 0) {
116
+ this.activeCheckpointId = rows[0].value as string
117
+ this.logger.info('Restored active checkpoint', { checkpointId: this.activeCheckpointId })
118
+ }
119
+
120
+ // Restore client host name on reconnect
121
+ const hostNameRows = [...this.sql`SELECT value FROM meta WHERE key = 'clientHostName'`]
122
+ if (hostNameRows.length > 0) {
123
+ this.clientHostName = hostNameRows[0].value as MCP_APP_HOST_NAMES
124
+ this.logger.info(`Restored client host name: ${this.clientHostName}`)
125
+ }
126
+
127
+ // Restore or generate session ID
128
+ const sessionRows = [...this.sql`SELECT value FROM meta WHERE key = 'sessionId'`]
129
+ if (sessionRows.length > 0) {
130
+ this.sessionId = sessionRows[0].value as string
131
+ } else {
132
+ this.sessionId = crypto.randomUUID()
133
+ void this
134
+ .sql`INSERT OR REPLACE INTO meta (key, value) VALUES ('sessionId', ${this.sessionId})`
135
+
136
+ // Track new session
137
+ this.env.MCP_ANALYTICS?.writeDataPoint({
138
+ blobs: ['session_start', this.sessionId],
139
+ doubles: [Date.now()],
140
+ })
141
+ }
142
+
143
+ // --- Widget HTML (loaded once from Assets binding) ---
144
+ const widgetHtml = await loadWidgetHtml(this.env.ASSETS)
145
+ let editorApiSpecPromise: ReturnType<typeof loadEditorApiSpecFromAssets> | null = null
146
+ let methodMapPromise: ReturnType<typeof loadMethodMapFromAssets> | null = null
147
+
148
+ // --- Build ServerDeps from SQLite ---
149
+ const deps: ServerDeps = {
150
+ saveCheckpoint: (id, shapes, assets = [], bindings = []) =>
151
+ this.saveCheckpoint(id, shapes, assets, bindings),
152
+ loadCheckpoint: (id) => this.loadCheckpoint(id),
153
+ getActiveCheckpointId: () => this.activeCheckpointId,
154
+ setActiveCheckpointId: (id) => {
155
+ this.activeCheckpointId = id
156
+ void this.sql`INSERT OR REPLACE INTO meta (key, value) VALUES ('activeCheckpointId', ${id})`
157
+ },
158
+ getCanvasCheckpointId: (canvasId) => {
159
+ const rows = [
160
+ ...this.sql`SELECT checkpoint_id FROM canvas_checkpoints WHERE canvas_id = ${canvasId}`,
161
+ ]
162
+ return rows.length > 0 ? (rows[0].checkpoint_id as string) : null
163
+ },
164
+ setCanvasCheckpointId: (canvasId, checkpointId) => {
165
+ void this
166
+ .sql`INSERT OR REPLACE INTO canvas_checkpoints (canvas_id, checkpoint_id) VALUES (${canvasId}, ${checkpointId})`
167
+ },
168
+ setPendingBootstrap: (bootstrap) => {
169
+ this.pendingBootstrap = bootstrap
170
+ },
171
+ consumePendingBootstrap: () => {
172
+ const b = this.pendingBootstrap
173
+ this.pendingBootstrap = null
174
+ return b
175
+ },
176
+ getSessionId: () => this.sessionId,
177
+ getMcpSessionId: () => this.getMcpSessionId(),
178
+ loadWidgetHtml: async () => widgetHtml,
179
+ loadEditorApiSpec: async () => {
180
+ editorApiSpecPromise ??= loadEditorApiSpecFromAssets(this.env.ASSETS)
181
+ return editorApiSpecPromise
182
+ },
183
+ loadMethodMap: async () => {
184
+ methodMapPromise ??= loadMethodMapFromAssets(this.env.ASSETS)
185
+ return methodMapPromise
186
+ },
187
+ }
188
+
189
+ const workerOrigin = this.env.WORKER_ORIGIN
190
+
191
+ registerTools(this.server, deps, {
192
+ log: this.logger.toLogFn(),
193
+ extraResourceDomains: workerOrigin ? [workerOrigin] : [],
194
+ extraConnectDomains: workerOrigin ? [workerOrigin] : [],
195
+ searchWorkerLoader: this.env.LOADER,
196
+ workerOrigin,
197
+ isDev: this.isDev,
198
+ analytics: this.env.MCP_ANALYTICS,
199
+ getClientHostName: () => this.clientHostName,
200
+ pendingRequests: this.pendingRequests,
201
+ })
202
+ }
203
+
204
+ // --- Checkpoint helpers ---
205
+
206
+ saveCheckpoint(id: string, shapes: unknown[], assets: unknown[] = [], bindings: unknown[] = []) {
207
+ const data = JSON.stringify({ shapes, assets, bindings })
208
+ void this
209
+ .sql`INSERT OR REPLACE INTO checkpoints (id, data, created_at) VALUES (${id}, ${data}, ${Date.now()})`
210
+ this.activeCheckpointId = id
211
+ void this.sql`INSERT OR REPLACE INTO meta (key, value) VALUES ('activeCheckpointId', ${id})`
212
+
213
+ // Evict old checkpoints beyond MAX_CHECKPOINTS (LRU)
214
+ void this
215
+ .sql`DELETE FROM checkpoints WHERE id NOT IN (SELECT id FROM checkpoints ORDER BY created_at DESC LIMIT ${MAX_CHECKPOINTS})`
216
+
217
+ this.logger.debug('Checkpoint saved', { checkpointId: id, shapes: shapes.length })
218
+ }
219
+
220
+ loadCheckpoint(id: string): { shapes: unknown[]; assets: unknown[]; bindings: unknown[] } | null {
221
+ const rows = [...this.sql`SELECT data FROM checkpoints WHERE id = ${id}`]
222
+ if (rows.length === 0) return null
223
+ const parsed = JSON.parse(rows[0].data as string)
224
+ // Backwards compat: old checkpoints stored a plain array of shapes
225
+ if (Array.isArray(parsed)) return { shapes: parsed, assets: [], bindings: [] }
226
+ return {
227
+ shapes: parsed.shapes ?? [],
228
+ assets: parsed.assets ?? [],
229
+ bindings: parsed.bindings ?? [],
230
+ }
231
+ }
232
+ }
233
+
234
+ // --- Fetch handler ---
235
+ // McpAgent.serve() handles CORS, session management, and transport internally.
236
+ // Expose both transports: Streamable HTTP at /mcp, SSE at /sse.
237
+
238
+ const mcpHandler = TldrawMCP.serve('/mcp')
239
+ const sseHandler = TldrawMCP.serveSSE('/sse')
240
+
241
+ export default {
242
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
243
+ try {
244
+ const requireAuth = Boolean(env.MCP_AUTH_TOKEN)
245
+ const url = new URL(request.url)
246
+
247
+ // CORS preflight
248
+ if (request.method === 'OPTIONS') {
249
+ return new Response(null, { status: 204, headers: CORS_HEADERS })
250
+ }
251
+
252
+ // Health check (no auth)
253
+ if (url.pathname === '/health') {
254
+ return Response.json({ status: 'ok', timestamp: Date.now() })
255
+ }
256
+
257
+ // Domain verification (no auth)
258
+ if (url.pathname === '/.well-known/openai-apps-challenge') {
259
+ return new Response('SG4iyi_lKvsvOJA-QN3UOJZeISqeAf4tnnxqgRMTU0k', {
260
+ headers: { 'Content-Type': 'text/plain' },
261
+ })
262
+ }
263
+
264
+ // Require bearer auth only when an auth token is configured.
265
+ if (requireAuth) {
266
+ const auth = request.headers.get('Authorization')
267
+ if (auth !== `Bearer ${env.MCP_AUTH_TOKEN}`) {
268
+ return corsResponse(new Response('Unauthorized', { status: 401 }))
269
+ }
270
+ }
271
+
272
+ // SSE transport (legacy)
273
+ if (url.pathname === '/sse' || url.pathname.startsWith('/sse/')) {
274
+ return sseHandler.fetch(request, env, ctx)
275
+ }
276
+
277
+ // Streamable HTTP transport
278
+ if (url.pathname === '/mcp' || url.pathname.startsWith('/mcp/')) {
279
+ const sessionId = request.headers.get('mcp-session-id')
280
+ const forwardedFor = request.headers.get('x-forwarded-for')
281
+ const clientIp =
282
+ request.headers.get('cf-connecting-ip') ?? forwardedFor?.split(',')[0]?.trim()
283
+ const rateLimitKey = sessionId
284
+ ? `mcp-session:${sessionId}`
285
+ : `mcp-ip:${clientIp ?? 'unknown'}`
286
+
287
+ const { success } = await env.RATE_LIMITER.limit({ key: rateLimitKey })
288
+ if (!success) {
289
+ return corsResponse(new Response('Rate limited', { status: 429 }))
290
+ }
291
+
292
+ // POST without a session ID is the initial handshake.
293
+ if (!sessionId && request.method !== 'POST') {
294
+ return corsResponse(new Response('Missing session', { status: 400 }))
295
+ }
296
+ return mcpHandler.fetch(request, env, ctx)
297
+ }
298
+
299
+ return new Response('Not found', { status: 404 })
300
+ } catch (err) {
301
+ console.error(err)
302
+ return new Response('Internal Server Error', { status: 500 })
303
+ }
304
+ },
305
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "jsx": "react-jsx",
11
+ "sourceMap": true,
12
+ "outDir": "dist",
13
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
14
+ "types": ["vite/client"]
15
+ },
16
+ "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"],
17
+ "exclude": ["node_modules", "dist"],
18
+ "references": [
19
+ {
20
+ "path": "../../packages/tldraw"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,13 @@
1
+ import react from '@vitejs/plugin-react'
2
+ import { defineConfig } from 'vite'
3
+ import { viteSingleFile } from 'vite-plugin-singlefile'
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), viteSingleFile()],
7
+ root: 'src/widget',
8
+ envDir: '../..',
9
+ build: {
10
+ outDir: '../../dist',
11
+ emptyOutDir: false,
12
+ },
13
+ })
@@ -0,0 +1,45 @@
1
+ name = "tldraw-mcp-app"
2
+ main = "src/worker.ts"
3
+ # Keep in sync with WORKER_COMPATIBILITY_DATE in src/shared/types.ts
4
+ compatibility_date = "2025-03-10"
5
+ compatibility_flags = ["nodejs_compat"]
6
+ preview_urls = true
7
+
8
+ [assets]
9
+ directory = "./dist"
10
+ binding = "ASSETS"
11
+ run_worker_first = true
12
+
13
+ [durable_objects]
14
+ bindings = [{ class_name = "TldrawMCP", name = "MCP_OBJECT" }]
15
+
16
+ [[migrations]]
17
+ tag = "v1"
18
+ new_sqlite_classes = ["TldrawMCP"]
19
+
20
+ [vars]
21
+ # Set this to the public URL of the worker. Used for constructing absolute image URLs.
22
+ # Override with `--var WORKER_ORIGIN:https://your-tunnel.trycloudflare.com` for cloudflared tunnels.
23
+ WORKER_ORIGIN = "https://tldraw-mcp-app.tldraw.workers.dev"
24
+ # Default to production-safe widget behavior. Local dev scripts override this with
25
+ # `--var MCP_IS_DEV:true` so local HTTP connectors omit `ui.domain`.
26
+ MCP_IS_DEV = "false"
27
+
28
+ [[analytics_engine_datasets]]
29
+ binding = "MCP_ANALYTICS"
30
+ dataset = "MCP_ANALYTICS"
31
+
32
+ [[worker_loaders]]
33
+ binding = "LOADER"
34
+
35
+ #################### Rate limiting ####################
36
+ [[ratelimits]]
37
+ name = "RATE_LIMITER"
38
+ namespace_id = "2001"
39
+
40
+ [ratelimits.simple]
41
+ limit = 30
42
+ period = 60
43
+
44
+ [observability]
45
+ enabled = true
@@ -0,0 +1,36 @@
1
+ {
2
+ "repo": "https://github.com/tldraw/tldraw.git",
3
+ "commit": "4b0cfc539e074217e1e248461afa596fc7d02040",
4
+ "appPath": "apps/mcp-app",
5
+ "patches": [
6
+ "patches/tldraw-mcp-app/001-pi-runtime.patch"
7
+ ],
8
+ "build": [
9
+ {
10
+ "cwd": ".",
11
+ "command": "yarn install --immutable"
12
+ },
13
+ {
14
+ "cwd": "apps/mcp-app",
15
+ "command": "yarn build"
16
+ }
17
+ ],
18
+ "assembledFiles": [
19
+ "LICENSE.md",
20
+ "README.md",
21
+ "package.json",
22
+ "server.json",
23
+ "tsconfig.json",
24
+ "vite.config.ts",
25
+ "wrangler.toml",
26
+ "dev-tunnel.sh",
27
+ "src/",
28
+ "scripts/",
29
+ "plugins/",
30
+ "dist/"
31
+ ],
32
+ "notes": [
33
+ "The bundled mcp-app/ keeps the existing runtime contract: cd mcp-app && yarn dev.",
34
+ "The patch preserves Pi autosave of assets/bindings and iframe fullscreen sizing fixes."
35
+ ]
36
+ }
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "pi-tldraw",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Pi extension that opens and persists a local tldraw MCP canvas.",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Zeebee Siwiec",
9
+ "email": "zeebee@dicr.tech",
10
+ "url": "https://github.com/zeebeeCoder"
11
+ },
12
+ "contributors": [
13
+ {
14
+ "name": "Zeebee Siwiec",
15
+ "email": "zeebee@dicr.tech",
16
+ "url": "https://github.com/zeebeeCoder"
17
+ }
18
+ ],
19
+ "keywords": [
20
+ "pi-package",
21
+ "pi",
22
+ "pi-extension",
23
+ "mcp",
24
+ "mcp-app",
25
+ "tldraw"
26
+ ],
27
+ "files": [
28
+ "src/",
29
+ "bridge/",
30
+ "scripts/",
31
+ "static/",
32
+ "mcp-app/",
33
+ "README.md",
34
+ "tsconfig.json",
35
+ "LICENSE",
36
+ "mcp-app-source.json",
37
+ "patches/"
38
+ ],
39
+ "engines": {
40
+ "node": ">=22.19.0"
41
+ },
42
+ "pi": {
43
+ "extensions": [
44
+ "./src/index.ts"
45
+ ]
46
+ },
47
+ "scripts": {
48
+ "typecheck": "tsc --noEmit",
49
+ "test": "vitest run",
50
+ "check": "npm run typecheck && npm test && npm run verify:bundle && npm run verify:mcp-app && npm run verify:mcp-app-source && npm run verify:package",
51
+ "pack:dry": "npm run build && npm run verify:package",
52
+ "prepublishOnly": "npm run build && npm run check",
53
+ "build:bridge": "node scripts/build-bridge.mjs",
54
+ "build": "npm run build:bridge",
55
+ "verify:bundle": "node scripts/verify-bundle.mjs",
56
+ "e2e:mcp": "node scripts/e2e-mcp.mjs",
57
+ "assemble:mcp-app": "node scripts/assemble-mcp-app.mjs",
58
+ "verify:mcp-app": "node scripts/verify-mcp-app.mjs",
59
+ "e2e:packaged-mcp-app": "node scripts/e2e-packaged-mcp-app.mjs",
60
+ "assemble:mcp-app:pinned": "node scripts/assemble-mcp-app.mjs --pinned",
61
+ "verify:mcp-app-source": "node scripts/verify-mcp-app-source.mjs",
62
+ "verify:package": "node scripts/verify-package-files.mjs"
63
+ },
64
+ "devDependencies": {
65
+ "@types/node": "^24.0.0",
66
+ "esbuild": "^0.28.1",
67
+ "typebox": "*",
68
+ "typescript": "^5.8.3",
69
+ "vitest": "^3.2.4"
70
+ },
71
+ "peerDependencies": {
72
+ "@earendil-works/pi-coding-agent": "*",
73
+ "typebox": "*"
74
+ },
75
+ "repository": {
76
+ "type": "git",
77
+ "url": "git+ssh://git@github.com/zeebeeCoder/pi-tldraw.git"
78
+ },
79
+ "bugs": {
80
+ "url": "https://github.com/zeebeeCoder/pi-tldraw/issues"
81
+ },
82
+ "homepage": "https://github.com/zeebeeCoder/pi-tldraw#readme",
83
+ "dependencies": {
84
+ "@modelcontextprotocol/ext-apps": "^1.7.4",
85
+ "@modelcontextprotocol/sdk": "^1.29.0",
86
+ "agents": "^0.5.0",
87
+ "wrangler": "4.75.0",
88
+ "zod": "^4.4.3"
89
+ }
90
+ }
@@ -0,0 +1,35 @@
1
+ diff --git a/apps/mcp-app/src/widget/mcp-app.css b/apps/mcp-app/src/widget/mcp-app.css
2
+ index 03f10d198..1bc9271bb 100644
3
+ --- a/apps/mcp-app/src/widget/mcp-app.css
4
+ +++ b/apps/mcp-app/src/widget/mcp-app.css
5
+ @@ -80,6 +80,8 @@
6
+ position: fixed;
7
+ inset: 0;
8
+ z-index: 9999;
9
+ + height: 100%;
10
+ + min-height: 0;
11
+ background: var(--tl-color-background);
12
+ }
13
+
14
+ diff --git a/apps/mcp-app/src/widget/persistence.ts b/apps/mcp-app/src/widget/persistence.ts
15
+ index a65d1cb14..feb5244f7 100644
16
+ --- a/apps/mcp-app/src/widget/persistence.ts
17
+ +++ b/apps/mcp-app/src/widget/persistence.ts
18
+ @@ -259,6 +259,8 @@ export function pushCanvasContext(app: App, editor: Editor, opts?: { message?: s
19
+ const shapes = [...editor.getCurrentPageShapes()].map((shape) =>
20
+ convertTldrawShapeToFocusedShape(editor, shape)
21
+ )
22
+ + const assets = [...editor.getAssets()].map((asset) => structuredClone(asset))
23
+ + const bindings = getEditorBindings(editor)
24
+
25
+ const canvasStatus = shapes.length > 0 ? `Current canvas state is attached.` : 'Canvas is empty.'
26
+
27
+ @@ -268,6 +270,8 @@ export function pushCanvasContext(app: App, editor: Editor, opts?: { message?: s
28
+ content: [{ type: 'text', text }],
29
+ structuredContent: {
30
+ shapes,
31
+ + assets,
32
+ + bindings,
33
+ },
34
+ })
35
+ }