owenkaplinsky commited on
Commit
0ca2c7f
·
1 Parent(s): 7660e27

Add MCP block and fix issues

Browse files
project/app.py CHANGED
@@ -2,9 +2,6 @@ from fastapi import FastAPI, Request
2
  from fastapi.middleware.cors import CORSMiddleware
3
  import gradio as gr
4
  import uvicorn
5
- import asyncio
6
- import threading
7
- import os
8
  from dotenv import load_dotenv
9
 
10
  app = FastAPI()
@@ -19,15 +16,7 @@ app.add_middleware(
19
 
20
  load_dotenv()
21
 
22
- history = []
23
  latest_blockly_code = ""
24
- assistant_queue = asyncio.Queue()
25
-
26
-
27
- async def reply(message):
28
- global history, assistant_queue
29
- history.append({"role": "assistant", "content": message})
30
- await assistant_queue.put(message)
31
 
32
 
33
  @app.post("/update_code")
@@ -39,61 +28,120 @@ async def update_code(request: Request):
39
  return {"ok": True}
40
 
41
 
42
- def execute_blockly_logic(user_message: str, loop):
43
- global latest_blockly_code, history
44
  if not latest_blockly_code.strip():
45
- return
46
 
47
- def safe_reply(msg):
48
- asyncio.run_coroutine_threadsafe(reply(msg), loop)
 
 
 
49
 
50
  env = {
51
- "reply": safe_reply,
52
- "history": history,
53
- "print": print,
54
  }
55
 
56
  try:
57
  exec(latest_blockly_code, env)
58
- if "on_user_send" in env:
59
- env["on_user_send"](user_message)
 
 
 
 
 
60
  except Exception as e:
61
  print("[EXECUTION ERROR]", e)
 
62
 
63
-
64
- def run_blockly_thread(user_message):
65
- loop = asyncio.get_running_loop()
66
- thread = threading.Thread(target=execute_blockly_logic, args=(user_message, loop))
67
- thread.start()
68
 
69
 
70
  def build_interface():
71
  with gr.Blocks() as demo:
72
- chatbot = gr.Chatbot(type="messages", label="Assistant", group_consecutive_messages=False)
73
- msg = gr.Textbox(placeholder="Type a message and press Enter")
74
-
75
- async def process_message(message):
76
- global history, assistant_queue
77
- history.append({"role": "user", "content": message})
78
- print(f"[USER] {message!r}")
79
- yield "", history
80
-
81
- while not assistant_queue.empty():
82
- assistant_queue.get_nowait()
83
-
84
- run_blockly_thread(message)
85
-
86
- while True:
87
- try:
88
- reply_text = await asyncio.wait_for(assistant_queue.get(), timeout=2)
89
- print(f"[ASSISTANT STREAM] {reply_text!r}")
90
- yield "", history
91
- except asyncio.TimeoutError:
92
- break
93
-
94
- msg.submit(process_message, [msg], [msg, chatbot], queue=True)
95
- clear_btn = gr.Button("Reset chat")
96
- clear_btn.click(lambda: ([], ""), None, [chatbot, msg], queue=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  return demo
99
 
 
2
  from fastapi.middleware.cors import CORSMiddleware
3
  import gradio as gr
4
  import uvicorn
 
 
 
5
  from dotenv import load_dotenv
6
 
7
  app = FastAPI()
 
16
 
17
  load_dotenv()
18
 
 
19
  latest_blockly_code = ""
 
 
 
 
 
 
 
20
 
21
 
22
  @app.post("/update_code")
 
28
  return {"ok": True}
29
 
30
 
31
+ def execute_blockly_logic(user_inputs):
32
+ global latest_blockly_code
33
  if not latest_blockly_code.strip():
34
+ return "No Blockly code available"
35
 
36
+ result = ""
37
+
38
+ def capture_result(msg):
39
+ nonlocal result
40
+ result = msg
41
 
42
  env = {
43
+ "reply": capture_result,
 
 
44
  }
45
 
46
  try:
47
  exec(latest_blockly_code, env)
48
+ if "create_mcp" in env:
49
+ # If create_mcp function exists, call it with the input arguments
50
+ # Filter out None values and convert to list for unpacking
51
+ args = [arg for arg in user_inputs if arg is not None]
52
+ result = env["create_mcp"](*args)
53
+ elif "process_input" in env:
54
+ env["process_input"](user_inputs)
55
  except Exception as e:
56
  print("[EXECUTION ERROR]", e)
57
+ result = f"Error: {str(e)}"
58
 
59
+ return result if result else "No output generated"
 
 
 
 
60
 
61
 
62
  def build_interface():
63
  with gr.Blocks() as demo:
64
+ gr.Markdown("# Blockly Code Executor")
65
+
66
+ # Create a fixed number of potential input fields (max 10)
67
+ input_fields = []
68
+ input_labels = []
69
+ input_group_items = []
70
+
71
+ with gr.Accordion("MCP Inputs", open=True):
72
+ for i in range(10):
73
+ # Create inputs that can be shown/hidden
74
+ txt = gr.Textbox(label=f"Input {i+1}", visible=False)
75
+ input_fields.append(txt)
76
+ input_group_items.append(txt)
77
+
78
+ output_text = gr.Textbox(label="Output", interactive=False)
79
+
80
+ with gr.Row():
81
+ submit_btn = gr.Button("Submit")
82
+ refresh_btn = gr.Button("Refresh Inputs")
83
+
84
+ def refresh_inputs():
85
+ global latest_blockly_code
86
+
87
+ # Parse the Python code to extract function parameters
88
+ import re
89
+
90
+ # Look for the create_mcp function definition
91
+ pattern = r'def create_mcp\((.*?)\):'
92
+ match = re.search(pattern, latest_blockly_code)
93
+
94
+ params = []
95
+ if match:
96
+ params_str = match.group(1)
97
+ if params_str.strip():
98
+ # Parse parameters to extract names and types
99
+ for param in params_str.split(','):
100
+ param = param.strip()
101
+ if ':' in param:
102
+ name, type_hint = param.split(':')
103
+ params.append({
104
+ 'name': name.strip(),
105
+ 'type': type_hint.strip()
106
+ })
107
+ else:
108
+ params.append({
109
+ 'name': param,
110
+ 'type': 'str'
111
+ })
112
+
113
+ # Generate visibility and label updates for each input field
114
+ updates = []
115
+ for i, field in enumerate(input_fields):
116
+ if i < len(params):
117
+ # Show this field and update its label
118
+ param = params[i]
119
+ updates.append(gr.update(
120
+ visible=True,
121
+ label=f"{param['name']} ({param['type']})"
122
+ ))
123
+ else:
124
+ # Hide this field
125
+ updates.append(gr.update(visible=False))
126
+
127
+ return updates
128
+
129
+ def process_input(*args):
130
+ # Get the input values from Gradio fields
131
+ return execute_blockly_logic(args)
132
+
133
+ # When refresh is clicked, update input field visibility and labels
134
+ refresh_btn.click(
135
+ refresh_inputs,
136
+ outputs=input_fields,
137
+ queue=False
138
+ )
139
+
140
+ submit_btn.click(
141
+ process_input,
142
+ inputs=input_fields,
143
+ outputs=[output_text]
144
+ )
145
 
146
  return demo
147
 
project/src/blocks/text.js CHANGED
@@ -1,89 +1,369 @@
1
  import * as Blockly from 'blockly/core';
 
2
 
3
- const whenUserSends = {
4
- type: 'when_user_sends',
5
- message0: 'when user sends %1 do %2 %3',
6
- args0: [
7
- {
8
- "type": "input_value",
9
- "name": "DUPLICATE"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  {
12
  "type": "input_dummy"
13
  },
14
  {
15
  "type": "input_statement",
16
- "name": "CODE"
17
- }
 
 
 
 
18
  ],
19
- inputsInline: true,
20
- colour: 230,
21
- tooltip: 'Triggered when the user sends a chat message.',
22
- helpUrl: '',
 
 
 
 
23
  };
24
 
25
- const assistantReply = {
26
- type: 'assistant_reply',
27
- message0: 'reply with %1',
28
- args0: [
 
29
  {
30
- type: 'input_value',
31
- name: 'INPUT',
32
- check: 'String',
33
  },
 
 
 
 
34
  ],
35
- previousStatement: null,
36
- nextStatement: null,
37
- colour: 65,
38
- tooltip: 'Send a message as the assistant.',
39
- helpUrl: '',
40
- };
41
 
42
- const getAssistantResponse = {
43
- type: 'get_assistant_response',
44
- message0: 'call model %1 with prompt %2 %3 history',
 
 
45
  args0: [
46
  {
47
  type: 'field_dropdown',
48
- name: 'MODEL',
49
  options: [
50
- ['gpt-3.5-turbo', 'gpt-3.5-turbo-0125'],
51
- ['gpt-5-mini', 'gpt-5-mini-2025-08-07'],
52
- ],
 
53
  },
54
  {
55
- type: 'input_value',
56
- name: 'PROMPT',
57
- check: 'String',
58
- },
59
- {
60
- type: 'field_dropdown',
61
- name: 'HISTORY',
62
- options: [
63
- ["with", "True"],
64
- ["without", "False"]
65
- ]
66
  },
67
  ],
68
- inputsInline: true,
69
- output: 'String',
70
- colour: 230,
71
- tooltip: 'Call the selected OpenAI model to get a response.',
72
- helpUrl: '',
73
  };
74
 
75
- const user_message = {
76
- type: "user_message",
77
- message0: "user message",
78
- output: "String",
79
- colour: "#47A8D1",
80
- tooltip: "",
81
- helpUrl: "",
82
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
 
84
  export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
85
- whenUserSends,
86
- assistantReply,
87
- getAssistantResponse,
88
- user_message
89
  ]);
 
1
  import * as Blockly from 'blockly/core';
2
+ import { pythonGenerator } from 'blockly/python';
3
 
4
+ // Function to create a unique block type for an input
5
+ function createInputRefBlockType(inputName) {
6
+ const blockType = `input_reference_${inputName}`;
7
+
8
+ // Only create if not already defined
9
+ if (!Blockly.Blocks[blockType]) {
10
+ // Define block
11
+ Blockly.Blocks[blockType] = {
12
+ init: function () {
13
+ this.jsonInit({
14
+ "type": blockType,
15
+ "message0": "%1",
16
+ "args0": [
17
+ {
18
+ "type": "field_label_serializable",
19
+ "name": "VARNAME",
20
+ "text": inputName
21
+ }
22
+ ],
23
+ "output": null,
24
+ "colour": 210,
25
+ "outputShape": 1 // Oval shape (1 = round)
26
+ });
27
+ }
28
+ };
29
+
30
+ // Define Python generator for this block type
31
+ pythonGenerator.forBlock[blockType] = function (block) {
32
+ // Just return the input name as a variable reference
33
+ return [inputName, pythonGenerator.ORDER_ATOMIC];
34
+ };
35
+ }
36
+ return blockType;
37
+ }
38
+
39
+ // Keep track of input reference blocks
40
+ const inputRefs = new Map(); // Maps input name to block ID
41
+
42
+ Blockly.Extensions.registerMutator(
43
+ 'test_mutator',
44
+ {
45
+ // Initialize tracking of input reference blocks
46
+ initialize: function () {
47
+ if (!this.initialized_) {
48
+ this.inputCount_ = 0;
49
+ this.inputNames_ = [];
50
+ this.inputTypes_ = [];
51
+ this.inputRefBlocks_ = new Map(); // Maps input name to block reference
52
+ this.initialized_ = true;
53
+ }
54
+ },
55
+
56
+ decompose: function (workspace) {
57
+ var containerBlock = workspace.newBlock('container');
58
+ containerBlock.initSvg();
59
+ var connection = containerBlock.getInput('STACK').connection;
60
+
61
+ // Ensure inputCount_ is defined
62
+ this.inputCount_ = this.inputCount_ || 0;
63
+ this.inputNames_ = this.inputNames_ || [];
64
+ this.inputTypes_ = this.inputTypes_ || [];
65
+
66
+ // Dynamically add input blocks based on inputCount_
67
+ for (var i = 0; i < this.inputCount_; i++) {
68
+ var itemBlock = workspace.newBlock('container_input');
69
+ itemBlock.initSvg();
70
+
71
+ var typeVal = this.inputTypes_[i] || 'string';
72
+ var nameVal = this.inputNames_[i] || typeVal;
73
+ itemBlock.setFieldValue(typeVal, 'TYPE'); // Set data type
74
+ itemBlock.setFieldValue(nameVal, 'NAME'); // Set name from stored name
75
+
76
+ // Preserve any existing connection from this block's input
77
+ var input = this.getInput('X' + i);
78
+ if (input && input.connection && input.connection.targetConnection) {
79
+ itemBlock.valueConnection_ = input.connection.targetConnection;
80
+ }
81
+
82
+ connection.connect(itemBlock.previousConnection);
83
+ connection = itemBlock.nextConnection;
84
+ }
85
+
86
+ return containerBlock;
87
+ },
88
+
89
+ compose: function (containerBlock) {
90
+ // Disable events during mutator updates to prevent duplication
91
+ Blockly.Events.disable();
92
+
93
+ try {
94
+ // Ensure initialization
95
+ if (!this.initialized_) {
96
+ this.initialize();
97
+ }
98
+
99
+ var itemBlock = containerBlock.getInputTargetBlock('STACK');
100
+ var connections = [];
101
+ var oldNames = this.inputNames_ ? [...this.inputNames_] : []; // Copy old names for cleanup
102
+
103
+ // Collect input connections
104
+ while (itemBlock) {
105
+ connections.push(itemBlock.valueConnection_);
106
+ itemBlock = itemBlock.nextConnection && itemBlock.nextConnection.targetBlock();
107
+ }
108
+
109
+ var newCount = connections.length;
110
+ this.inputCount_ = newCount;
111
+ this.inputNames_ = this.inputNames_ || [];
112
+ this.inputTypes_ = this.inputTypes_ || [];
113
+
114
+ var idx = 0;
115
+ var it = containerBlock.getInputTargetBlock('STACK');
116
+ var newNames = [];
117
+ while (it) {
118
+ this.inputTypes_[idx] = it.getFieldValue('TYPE') || 'string';
119
+ this.inputNames_[idx] = it.getFieldValue('NAME') || ('arg' + idx);
120
+ newNames.push(this.inputNames_[idx]);
121
+ it = it.nextConnection && it.nextConnection.targetBlock();
122
+ idx++;
123
+ }
124
+
125
+ // Clean up removed input reference blocks only if count decreased
126
+ if (newCount < oldNames.length) {
127
+ // Only remove blocks for inputs that were deleted (beyond new count)
128
+ for (let i = newCount; i < oldNames.length; i++) {
129
+ const oldName = oldNames[i];
130
+ const block = this.inputRefBlocks_.get(oldName);
131
+ if (block && !block.disposed) {
132
+ block.dispose(true); // true = skip gap
133
+ }
134
+ this.inputRefBlocks_.delete(oldName);
135
+ }
136
+ }
137
+
138
+ // Handle renamed variables - update ALL instances (main block + clones)
139
+ for (let i = 0; i < Math.min(oldNames.length, newNames.length); i++) {
140
+ const oldName = oldNames[i];
141
+ const newName = newNames[i];
142
+ if (oldName !== newName) {
143
+ const oldBlockType = `input_reference_${oldName}`;
144
+ const newBlockType = `input_reference_${newName}`;
145
+
146
+ // Update the reference block in the MCP block
147
+ if (this.inputRefBlocks_.has(oldName)) {
148
+ const refBlock = this.inputRefBlocks_.get(oldName);
149
+ if (refBlock && !refBlock.disposed) {
150
+ this.inputRefBlocks_.delete(oldName);
151
+ this.inputRefBlocks_.set(newName, refBlock);
152
+ refBlock.setFieldValue(newName, 'VARNAME');
153
+ refBlock.type = newBlockType; // Update the block type
154
+ }
155
+ }
156
+
157
+ // Find and update ALL clones of this reference block in the workspace
158
+ const workspace = this.workspace;
159
+ const allBlocks = workspace.getAllBlocks(false);
160
+ for (const block of allBlocks) {
161
+ if (block.type === oldBlockType && block !== this.inputRefBlocks_.get(newName)) {
162
+ // This is a clone - update it too
163
+ block.type = newBlockType;
164
+ block.setFieldValue(newName, 'VARNAME');
165
+ }
166
+ }
167
+
168
+ // Update the Python generator for the new block type
169
+ pythonGenerator.forBlock[newBlockType] = function (block) {
170
+ return [newName, pythonGenerator.ORDER_ATOMIC];
171
+ };
172
+ }
173
+ }
174
+
175
+ // Remove only the dynamic inputs (X0, X1, etc.)
176
+ var i = 0;
177
+ while (this.getInput('X' + i)) {
178
+ this.removeInput('X' + i);
179
+ i++;
180
+ }
181
+
182
+ // Now add dynamic inputs at the correct position
183
+ for (var j = 0; j < newCount; j++) {
184
+ var type = this.inputTypes_[j] || 'string';
185
+ var name = this.inputNames_[j] || type;
186
+ var check = null;
187
+ if (type === 'integer') check = 'Number';
188
+ if (type === 'string') check = 'String';
189
+ // For list, leave check as null (no restriction)
190
+
191
+ // Get existing reference block if any
192
+ const existingRefBlock = this.inputRefBlocks_.get(name);
193
+
194
+ // Insert inputs at the beginning (after the static text)
195
+ var input = this.appendValueInput('X' + j);
196
+ if (check) input.setCheck(check);
197
+ input.appendField(type); // Display the type instead of the name
198
+
199
+ // Move the input to the correct position (after dummy inputs, before BODY)
200
+ this.moveInputBefore('X' + j, 'BODY');
201
+
202
+ // Create or reuse reference block
203
+ const blockType = createInputRefBlockType(name);
204
+ if (!existingRefBlock) {
205
+ // Create new reference block
206
+ const workspace = this.workspace;
207
+ const refBlock = workspace.newBlock(blockType);
208
+ refBlock.initSvg();
209
+ refBlock.setDeletable(false); // Can't be deleted directly
210
+ refBlock.render();
211
+ this.inputRefBlocks_.set(name, refBlock);
212
+
213
+ // Connect new block
214
+ if (input && input.connection && refBlock.outputConnection) {
215
+ input.connection.connect(refBlock.outputConnection);
216
+ }
217
+ } else {
218
+ // Reuse existing block - connect it to new input
219
+ if (input && input.connection && existingRefBlock.outputConnection) {
220
+ input.connection.connect(existingRefBlock.outputConnection);
221
+ }
222
+
223
+ // Update the Python generator for renamed variables
224
+ pythonGenerator.forBlock[blockType] = function (block) {
225
+ return [name, pythonGenerator.ORDER_ATOMIC];
226
+ };
227
+ }
228
+ }
229
+
230
+ // Reconnect preserved connections
231
+ for (var k = 0; k < newCount; k++) {
232
+ var conn = connections[k];
233
+ if (conn) {
234
+ try {
235
+ conn.connect(this.getInput('X' + k).connection);
236
+ } catch (e) {
237
+ // ignore failed reconnects
238
+ }
239
+ }
240
+ }
241
+
242
+ // Force workspace to update
243
+ this.workspace.render();
244
+
245
+ } finally {
246
+ // Re-enable events
247
+ Blockly.Events.enable();
248
+ }
249
+ },
250
+
251
+ saveExtraState: function () {
252
+ const state = {
253
+ inputCount: this.inputCount_,
254
+ inputNames: this.inputNames_,
255
+ inputTypes: this.inputTypes_
256
+ };
257
+ return state;
258
  },
259
+
260
+ loadExtraState: function (state) {
261
+ this.inputCount_ = state['inputCount'];
262
+ this.inputNames_ = state['inputNames'] || [];
263
+ this.inputTypes_ = state['inputTypes'] || [];
264
+ }
265
+ },
266
+ null, // No helper function needed
267
+ ['container_input']
268
+ );
269
+
270
+ // Define the test block with mutator
271
+ const create_mcp = {
272
+ "type": "create_mcp",
273
+ "message0": "create MCP with inputs: %1 %2 and return %3",
274
+ "args0": [
275
  {
276
  "type": "input_dummy"
277
  },
278
  {
279
  "type": "input_statement",
280
+ "name": "BODY"
281
+ },
282
+ {
283
+ "type": "input_value",
284
+ "name": "RETURN",
285
+ },
286
  ],
287
+ "colour": 160,
288
+ "inputsInline": true,
289
+ "mutator": "test_mutator",
290
+ "inputCount_": 0, // Start with no inputs
291
+ "deletable": false, // Make the block non-deletable
292
+
293
+ // Override the dispose function to clean up reference blocks
294
+ "extensions": ["test_cleanup_extension"]
295
  };
296
 
297
+ // Define the container block for the mutator
298
+ const container = {
299
+ "type": "container",
300
+ "message0": "inputs %1 %2",
301
+ "args0": [
302
  {
303
+ "type": "input_dummy",
304
+ "name": "title"
 
305
  },
306
+ {
307
+ "type": "input_statement",
308
+ "name": "STACK"
309
+ }
310
  ],
311
+ "colour": 160,
312
+ "inputsInline": false
313
+ }
 
 
 
314
 
315
+
316
+ // Define the input block for the mutator
317
+ const container_input = {
318
+ type: 'container_input',
319
+ message0: '%1 %2',
320
  args0: [
321
  {
322
  type: 'field_dropdown',
323
+ name: 'TYPE',
324
  options: [
325
+ ["String", "string"],
326
+ ["Integer", "integer"],
327
+ ["List", "list"],
328
+ ]
329
  },
330
  {
331
+ type: 'field_input',
332
+ name: 'NAME',
 
 
 
 
 
 
 
 
 
333
  },
334
  ],
335
+ previousStatement: null,
336
+ nextStatement: null,
337
+ colour: 210,
 
 
338
  };
339
 
340
+ // Register an extension to handle cleanup when the block is deleted
341
+ Blockly.Extensions.register('test_cleanup_extension', function () {
342
+ // Store the original dispose function
343
+ const oldDispose = this.dispose;
344
+
345
+ // Override the dispose function
346
+ this.dispose = function (healStack, recursive) {
347
+ // Clean up all reference blocks first
348
+ if (this.inputRefBlocks_) {
349
+ for (const [name, refBlock] of this.inputRefBlocks_) {
350
+ if (refBlock && !refBlock.disposed) {
351
+ refBlock.dispose(false); // Don't heal stack for reference blocks
352
+ }
353
+ }
354
+ this.inputRefBlocks_.clear();
355
+ }
356
+
357
+ // Call the original dispose function
358
+ if (oldDispose) {
359
+ oldDispose.call(this, healStack, recursive);
360
+ }
361
+ };
362
+ });
363
 
364
+ // Create block definitions from the JSON
365
  export const blocks = Blockly.common.createBlockDefinitionsFromJsonArray([
366
+ create_mcp,
367
+ container,
368
+ container_input,
 
369
  ]);
project/src/generators/python.js CHANGED
@@ -2,30 +2,58 @@ import { Order } from 'blockly/python';
2
 
3
  export const forBlock = Object.create(null);
4
 
5
- // Generates a Python function that runs when the user sends a message
6
- forBlock['when_user_sends'] = function (block, generator) {
7
- const body = generator.statementToCode(block, 'CODE') || "";
8
- const code = `def on_user_send(user_message):\n${body} return\n`;
9
- return code;
10
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- // Generates a Python 'return' statement for the assistant's reply
13
- forBlock['assistant_reply'] = function (block, generator) {
14
- const reply = generator.valueToCode(block, 'INPUT', Order.NONE) || "''";
15
- const code = `reply(${reply})\n`;
16
- return code;
17
- };
18
 
19
- forBlock['get_assistant_response'] = function (block, generator) {
20
- const model = block.getFieldValue('MODEL');
21
- const prompt = generator.valueToCode(block, 'PROMPT', Order.NONE) || "''";
22
- const history = block.getFieldValue('HISTORY');
23
 
24
- const code = `get_assistant_response(${prompt}, model="${model}", use_history=${history})`;
25
- return [code, Order.NONE];
26
- };
 
 
 
 
27
 
28
- forBlock['user_message'] = function (block, generator) {
29
- const code = `user_message`;
30
- return [code, generator.ORDER_NONE];
 
 
 
 
 
 
31
  };
 
2
 
3
  export const forBlock = Object.create(null);
4
 
5
+ forBlock['create_mcp'] = function (block, generator) {
6
+ // Get all inputs with their types
7
+ const typedInputs = [];
8
+ let i = 0;
9
+ while (block.getInput('X' + i)) {
10
+ const input = block.getInput('X' + i);
11
+ if (input && input.connection && input.connection.targetBlock()) {
12
+ // Get the actual parameter name from inputNames_ array
13
+ const paramName = (block.inputNames_ && block.inputNames_[i]) || ('arg' + i);
14
+ // Get the type from inputTypes_ array
15
+ const type = (block.inputTypes_ && block.inputTypes_[i]) || 'string';
16
+ // Convert type to Python type annotation
17
+ let pyType;
18
+ switch (type) {
19
+ case 'integer':
20
+ pyType = 'int';
21
+ break;
22
+ case 'string':
23
+ pyType = 'str';
24
+ break;
25
+ case 'list':
26
+ pyType = 'list';
27
+ break;
28
+ default:
29
+ pyType = 'Any';
30
+ }
31
+ typedInputs.push(`${paramName}: ${pyType}`);
32
+ }
33
+ i++;
34
+ }
35
 
36
+ // Get the code for blocks inside the BODY statement input
37
+ let body = generator.statementToCode(block, 'BODY');
 
 
 
 
38
 
39
+ // Get the return value if any
40
+ let returnValue = generator.valueToCode(block, 'RETURN', Order.ATOMIC);
 
 
41
 
42
+ // Replace arg references with actual parameter names
43
+ if (returnValue && block.inputNames_) {
44
+ for (let j = 0; j < block.inputNames_.length; j++) {
45
+ const paramName = block.inputNames_[j];
46
+ returnValue = returnValue.replace(new RegExp(`arg${j}\\b`, 'g'), paramName);
47
+ }
48
+ }
49
 
50
+ let returnStatement = returnValue ? ` return ${returnValue}\n` : ' return\n';
51
+
52
+ // Create the function with all typed inputs
53
+ if (typedInputs.length > 0) {
54
+ const code = `def create_mcp(${typedInputs.join(', ')}):\n${body}${returnStatement}\n`;
55
+ return code;
56
+ } else {
57
+ return `def create_mcp():\n${body}${returnStatement}`;
58
+ }
59
  };
project/src/index.js CHANGED
@@ -57,29 +57,8 @@ const updateCode = () => {
57
  let code = pythonGenerator.workspaceToCode(ws);
58
  const codeEl = document.querySelector('#generatedCode code');
59
 
60
- const response = `def get_assistant_response(prompt, model, use_history=True):
61
- global history
62
- from openai import OpenAI
63
- import os
64
-
65
- client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
66
-
67
- if use_history:
68
- messages = history + [{"role": "user", "content": prompt}]
69
- else:
70
- messages = [{"role": "user", "content": prompt}]
71
-
72
- completion = client.chat.completions.create(model=model, messages=messages)
73
- return completion.choices[0].message.content.strip()
74
-
75
- `;
76
-
77
  const blocks = ws.getAllBlocks(false);
78
- const hasResponse = blocks.some(block => block.type === 'assistant_reply');
79
-
80
- if (hasResponse) {
81
- code = response + code;
82
- }
83
 
84
  if (codeEl) {
85
  codeEl.textContent = code;
@@ -105,6 +84,21 @@ try {
105
  console.warn('Workspace load failed, clearing storage:', e);
106
  localStorage.clear();
107
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  updateCode();
109
 
110
  ws.addChangeListener((e) => {
@@ -124,24 +118,36 @@ ws.addChangeListener((event) => {
124
  if (
125
  removedBlock &&
126
  oldParent &&
127
- removedBlock.type === 'user_message' &&
128
- oldParent.type === 'when_user_sends'
129
  ) {
130
- console.log('[DISCONNECT] user_message removed from when_user_sends');
 
 
 
131
 
132
- if (event.oldInputName) {
133
  Blockly.Events.disable();
134
  try {
135
- const newMsgBlock = ws.newBlock('user_message');
136
- newMsgBlock.initSvg();
137
- newMsgBlock.render();
 
138
 
139
- const input = oldParent.getInput(event.oldInputName);
140
  if (input) {
141
- input.connection.connect(newMsgBlock.outputConnection);
142
- console.log('[REPLACE] user_message restored instantly (no blink)');
 
 
 
 
 
143
  }
144
 
 
 
 
145
  ws.render(); // ensure workspace updates immediately
146
  } finally {
147
  Blockly.Events.enable();
@@ -159,23 +165,4 @@ ws.addChangeListener((e) => {
159
  return;
160
  }
161
  updateCode();
162
- });
163
-
164
- window.addEventListener("message", (event) => {
165
- if (event.data?.type === "user_message") {
166
- handleUserMessage(event.data.text);
167
- }
168
- });
169
-
170
- function handleUserMessage(message) {
171
- const blocks = ws.getAllBlocks();
172
- const msgBlock = blocks.find(b => b.type === 'user_message');
173
- if (msgBlock) {
174
- msgBlock.setFieldValue(message, 'TEXT');
175
- console.log(`[Blockly] Updated user_message with: ${message}`);
176
- }
177
-
178
- if (typeof window.userSendHandler === "function") {
179
- window.userSendHandler(message);
180
- }
181
- }
 
57
  let code = pythonGenerator.workspaceToCode(ws);
58
  const codeEl = document.querySelector('#generatedCode code');
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  const blocks = ws.getAllBlocks(false);
61
+ const hasBlock = blocks.some(block => block.type === 'block');
 
 
 
 
62
 
63
  if (codeEl) {
64
  codeEl.textContent = code;
 
84
  console.warn('Workspace load failed, clearing storage:', e);
85
  localStorage.clear();
86
  }
87
+
88
+ // Ensure there's always one MCP block in the workspace
89
+ const existingMcpBlocks = ws.getBlocksByType('create_mcp');
90
+ if (existingMcpBlocks.length === 0) {
91
+ // Create the MCP block
92
+ const mcpBlock = ws.newBlock('create_mcp');
93
+ mcpBlock.initSvg();
94
+ mcpBlock.setDeletable(false);
95
+ mcpBlock.setMovable(true); // Allow moving but not deleting
96
+
97
+ // Position it in a reasonable spot
98
+ mcpBlock.moveBy(50, 50);
99
+ mcpBlock.render();
100
+ }
101
+
102
  updateCode();
103
 
104
  ws.addChangeListener((e) => {
 
118
  if (
119
  removedBlock &&
120
  oldParent &&
121
+ removedBlock.type.startsWith('input_reference_') &&
122
+ oldParent.type === 'create_mcp'
123
  ) {
124
+ // Only duplicate if removed from a mutator input (X0, X1, X2, etc.)
125
+ // NOT from other inputs like RETURN, BODY, or title input
126
+ const inputName = event.oldInputName;
127
+ const isMutatorInput = inputName && /^X\d+$/.test(inputName);
128
 
129
+ if (isMutatorInput) {
130
  Blockly.Events.disable();
131
  try {
132
+ // Create a new block of the same reference type
133
+ const newRefBlock = ws.newBlock(removedBlock.type);
134
+ newRefBlock.initSvg();
135
+ newRefBlock.setDeletable(false); // This one stays in the MCP block
136
 
137
+ const input = oldParent.getInput(inputName);
138
  if (input) {
139
+ input.connection.connect(newRefBlock.outputConnection);
140
+ }
141
+
142
+ // Update the parent block's reference tracking
143
+ const varName = removedBlock.type.replace('input_reference_', '');
144
+ if (oldParent.inputRefBlocks_) {
145
+ oldParent.inputRefBlocks_.set(varName, newRefBlock);
146
  }
147
 
148
+ // Make the dragged-out block deletable
149
+ removedBlock.setDeletable(true);
150
+
151
  ws.render(); // ensure workspace updates immediately
152
  } finally {
153
  Blockly.Events.enable();
 
165
  return;
166
  }
167
  updateCode();
168
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
project/src/toolbox.js CHANGED
@@ -9,45 +9,7 @@ export const toolbox = {
9
  {
10
  'kind': 'sep',
11
  },
12
- {
13
- kind: 'category',
14
- name: 'Custom',
15
- categorystyle: 'logic_category',
16
- contents: [
17
- {
18
- kind: 'block',
19
- type: 'when_user_sends',
20
- inputs: {
21
- DUPLICATE: {
22
- block: {
23
- kind: "block",
24
- type: "user_message"
25
- }
26
- },
27
- },
28
- },
29
- {
30
- kind: 'block',
31
- type: 'assistant_reply',
32
- },
33
- {
34
- kind: 'block',
35
- type: 'get_assistant_response',
36
- inputs: {
37
- PROMPT: {
38
- shadow: {
39
- kind: "block",
40
- type: "user_message"
41
- }
42
- },
43
- },
44
- },
45
- {
46
- kind: 'block',
47
- type: 'user_message',
48
- },
49
- ]
50
- },
51
  {
52
  kind: 'category',
53
  name: 'Logic',
 
9
  {
10
  'kind': 'sep',
11
  },
12
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  {
14
  kind: 'category',
15
  name: 'Logic',