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.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/bridge/app-bridge-entry.js +6 -0
- package/mcp-app/LICENSE.md +9 -0
- package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
- package/mcp-app/README.md +129 -0
- package/mcp-app/dev-tunnel.sh +51 -0
- package/mcp-app/dist/editor-api.json +8493 -0
- package/mcp-app/dist/mcp-app.html +643 -0
- package/mcp-app/dist/method-map.json +915 -0
- package/mcp-app/package.json +42 -0
- package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
- package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
- package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
- package/mcp-app/scripts/extract-editor-api.ts +1374 -0
- package/mcp-app/server.json +21 -0
- package/mcp-app/src/logger.ts +45 -0
- package/mcp-app/src/register-tools.ts +368 -0
- package/mcp-app/src/shared/generated-data.ts +160 -0
- package/mcp-app/src/shared/pending-requests.ts +69 -0
- package/mcp-app/src/shared/types.ts +76 -0
- package/mcp-app/src/shared/utils.ts +132 -0
- package/mcp-app/src/tools/exec.ts +120 -0
- package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
- package/mcp-app/src/tools/search.ts +150 -0
- package/mcp-app/src/widget/app-context.tsx +29 -0
- package/mcp-app/src/widget/dev-log.tsx +70 -0
- package/mcp-app/src/widget/exec-helpers.ts +232 -0
- package/mcp-app/src/widget/export-tldr.ts +35 -0
- package/mcp-app/src/widget/focused/defaults.ts +141 -0
- package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
- package/mcp-app/src/widget/focused/format.ts +366 -0
- package/mcp-app/src/widget/focused/to-focused.ts +258 -0
- package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
- package/mcp-app/src/widget/image-guard.tsx +106 -0
- package/mcp-app/src/widget/index.html +33 -0
- package/mcp-app/src/widget/mcp-app.css +113 -0
- package/mcp-app/src/widget/mcp-app.tsx +857 -0
- package/mcp-app/src/widget/persistence.ts +337 -0
- package/mcp-app/src/widget/snapshot.ts +157 -0
- package/mcp-app/src/worker.ts +305 -0
- package/mcp-app/tsconfig.json +23 -0
- package/mcp-app/vite.config.ts +13 -0
- package/mcp-app/wrangler.toml +45 -0
- package/mcp-app-source.json +36 -0
- package/package.json +90 -0
- package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
- package/scripts/assemble-mcp-app.mjs +193 -0
- package/scripts/build-bridge.mjs +74 -0
- package/scripts/e2e-mcp.mjs +69 -0
- package/scripts/e2e-packaged-mcp-app.mjs +79 -0
- package/scripts/run-mcp-app-dev.mjs +44 -0
- package/scripts/verify-bundle.mjs +41 -0
- package/scripts/verify-mcp-app-source.mjs +51 -0
- package/scripts/verify-mcp-app.mjs +38 -0
- package/scripts/verify-package-files.mjs +50 -0
- package/src/canvas/export.ts +164 -0
- package/src/canvas/state.ts +117 -0
- package/src/canvas/workflow.ts +105 -0
- package/src/commands/tldraw-command.ts +48 -0
- package/src/diagram/guidance.ts +44 -0
- package/src/host/local-host.ts +289 -0
- package/src/index.ts +762 -0
- package/src/mcp/client.ts +126 -0
- package/src/mcp/response.ts +74 -0
- package/src/semantic/layer.ts +309 -0
- package/src/server/server-manager.ts +153 -0
- package/src/store/export-store.ts +33 -0
- package/src/store/project-store.ts +251 -0
- package/src/ui/tldraw-status.ts +88 -0
- package/static/app-bridge-bundle.js +18114 -0
- package/static/app-bridge-bundle.meta.json +164 -0
- package/static/host.html +390 -0
- 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
|
+
}
|