json-object-editor 0.10.660 → 0.10.662

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.
@@ -170,7 +170,7 @@ var fields = {
170
170
  return status.color||'';
171
171
  }
172
172
  },
173
- tags:{type:'group',icon:'tag',hidden:function(obj){
173
+ tags:{type:'group',icon:'tag',reloadable:true,hidden:function(obj){
174
174
  let tags = _joe.Data.tag.filter(function(tag){
175
175
  return !(tag.datasets.indexOf(item.itemtype) == -1);
176
176
  });
@@ -542,6 +542,53 @@ var fields = {
542
542
  4.1-nano is best for lightweight classification or routing logic where speed and cost matter more than depth.`,
543
543
  default: "gpt-5-mini",
544
544
  },
545
+ mcp_enabled:{
546
+ type:'boolean',
547
+ display:'Enable MCP tools',
548
+ default:false,
549
+ comment:'When true, this surface may call JOE MCP tools (read-only by default unless toolset/custom overrides it).'
550
+ },
551
+ mcp_toolset:{
552
+ type:'select',
553
+ display:'MCP Toolset',
554
+ values:[
555
+ {value:'read-only',name:'Read-only (safe defaults)'},
556
+ {value:'minimal',name:'Minimal (small safe subset)'},
557
+ {value:'all',name:'All MCP tools'},
558
+ {value:'custom',name:'Custom selection'}
559
+ ],
560
+ default:'read-only',
561
+ rerender:'mcp_selected_tools',
562
+ comment:'Choose which MCP toolset to expose when MCP tools are enabled.'
563
+ },
564
+ mcp_selected_tools:{
565
+ type:'select',
566
+ display:'Custom MCP tools (names)',
567
+ isArray:true,
568
+ allowMultiple:true,
569
+ values:function(doc){
570
+ try{
571
+ return (window._joe && _joe.Ai && Array.isArray(_joe.Ai.mcpToolNames)) ? _joe.Ai.mcpToolNames : [];
572
+ }catch(e){
573
+ return [];
574
+ }
575
+ },
576
+ hidden:function(doc){
577
+ return !doc || doc.mcp_toolset !== 'custom';
578
+ },
579
+ comment:'Multi-select from MCP manifest tool names. Only used when MCP Toolset = custom.'
580
+ },
581
+ mcp_instructions_mode:{
582
+ type:'select',
583
+ display:'MCP Instructions Mode',
584
+ values:[
585
+ {value:'auto',name:'Auto (short per-tool instructions)'},
586
+ {value:'full',name:'Full MCP instructions block'},
587
+ {value:'none',name:'No MCP instructions (tools only)'}
588
+ ],
589
+ default:'auto',
590
+ comment:'Controls whether and how MCP tool instructions are appended to system text.'
591
+ },
545
592
  objectChat:{
546
593
  type:'button',
547
594
  display:'Start Chat',
@@ -1019,6 +1019,10 @@ MCP.buildToolInstructions = function(toolNames, mode){
1019
1019
  if (m === 'none') { return ''; }
1020
1020
 
1021
1021
  const lines = [];
1022
+ // Visual section break so humans (and logs/debug views) can easily
1023
+ // spot where the MCP tool list begins inside a larger system prompt.
1024
+ // This is purely presentational and does not affect tool behavior.
1025
+ lines.push('----- JOE MCP tools -----');
1022
1026
  lines.push('You can call the following JOE MCP tools when helpful:');
1023
1027
  toolNames.forEach(function(name){
1024
1028
  const desc = MCP.descriptions && MCP.descriptions[name] || '';
@@ -48,6 +48,9 @@ function getInclude(req,res,next){
48
48
  case 'css':
49
49
  res.set('Content-Type', 'text/css');
50
50
  break;
51
+ case 'json':
52
+ res.set('Content-Type', 'application/json');
53
+ break;
51
54
  }
52
55
  res.send(content);
53
56
  }
@@ -536,8 +539,31 @@ var routeWithoutSlash = route.startsWith('/') ? route.slice(1) : route;
536
539
  page.content = Editor.wrap(page.content,'jpe-content',{name:'page content'})
537
540
  }
538
541
 
539
- var template = layout.template.replace('${this.PAGE.content}',content.PAGE.content);
540
- var html = fillTemplate(template,content);
542
+ // First, process page content with fillTemplate to resolve variables like ${INCLUDES}, ${this.PAGE.name}, etc.
543
+ // This is needed because these variables are INSIDE the page content
544
+ var pageContent = content.PAGE.content || '';
545
+ pageContent = pageContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
546
+
547
+ // Preserve newlines before fillTemplate strips them
548
+ // Replace newlines with a temporary marker that we'll restore later
549
+ var newlineMarker = '__JOE_NEWLINE__' + Date.now() + '__';
550
+ var pageContentWithMarkers = pageContent.replace(/\n/g, newlineMarker);
551
+
552
+ // Process page content with fillTemplate to resolve template variables
553
+ var processedPageContent = fillTemplate(pageContentWithMarkers, content);
554
+
555
+ // Restore newlines after fillTemplate processing
556
+ processedPageContent = processedPageContent.replace(new RegExp(newlineMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '\n');
557
+
558
+ // Use a placeholder for ${this.PAGE.content} in layout template
559
+ var placeholder = '__JOE_PAGE_CONTENT_PLACEHOLDER__' + Date.now() + '__';
560
+ var template = layout.template.replace(/\$\{this\.PAGE\.content\}/g, placeholder);
561
+
562
+ // Process layout template with fillTemplate (which will strip newlines from layout, but our placeholder remains)
563
+ var html = fillTemplate(template, content);
564
+
565
+ // Replace placeholder with processed page content (which has newlines preserved)
566
+ html = html.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), processedPageContent);
541
567
  if(editor){
542
568
  html = html.replace('</head>',Editor.styles+'</head>')
543
569
  .replace('</body>',Editor.gui(layout)+Editor.gui(page)+Editor.menu
@@ -19,7 +19,28 @@ function ChatGPT() {
19
19
  var self = this;
20
20
  this.async ={};
21
21
  function coloredLog(message){
22
- console.log(JOE.Utils.color('[chatgpt]', 'plugin', false), message);
22
+ try{
23
+ // Only emit verbose plugin logs in non‑production environments.
24
+ // This keeps consoles clean in production while preserving rich
25
+ // traces (assistant resolution, MCP config, systemText) during
26
+ // local/dev debugging.
27
+ var env = null;
28
+ if (typeof JOE !== 'undefined' && JOE && JOE.webconfig && JOE.webconfig.env){
29
+ env = JOE.webconfig.env;
30
+ } else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV){
31
+ env = process.env.NODE_ENV;
32
+ }
33
+ if (env && env.toLowerCase() === 'production'){
34
+ return;
35
+ }
36
+ console.log(JOE.Utils.color('[chatgpt]', 'plugin', false), message);
37
+ }catch(_e){
38
+ // If anything goes wrong determining env, default to logging so
39
+ // that development debugging is not silently broken.
40
+ try{
41
+ console.log('[chatgpt]', message);
42
+ }catch(__e){}
43
+ }
23
44
  }
24
45
  //xx -setup and send a test prompt to chatgpt
25
46
  //xx get the api key from joe settings
@@ -58,6 +79,23 @@ function ChatGPT() {
58
79
  const summary = JOE.Schemas && JOE.Schemas.summary && JOE.Schemas.summary[name];
59
80
  return { full, summary };
60
81
  }
82
+ function buildMcpToolsFromConfig(cfg) {
83
+ if (!cfg || !cfg.mcp_enabled) {
84
+ return { tools: null, names: [] };
85
+ }
86
+ try {
87
+ const names = MCP.getToolNamesForToolset(
88
+ cfg.mcp_toolset || 'read-only',
89
+ Array.isArray(cfg.mcp_selected_tools) ? cfg.mcp_selected_tools : null
90
+ );
91
+ const defs = MCP.getToolDefinitions(names);
92
+ return { tools: defs, names: names };
93
+ } catch (e) {
94
+ console.warn('[chatgpt] buildMcpToolsFromConfig failed', e);
95
+ return { tools: null, names: [] };
96
+ }
97
+ }
98
+
61
99
  /**
62
100
  * callMCPTool
63
101
  *
@@ -270,29 +308,32 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
270
308
  const attachmentsMode = opts.attachments_mode || null;
271
309
  const openaiFileIds = opts.openai_file_ids || null;
272
310
 
273
- // Normalize tools: in many schemas tools may be stored as a JSON string;
274
- // here we accept either an array or a JSON-stringified array.
275
- let tools = null;
311
+ // Debug/trace: log the effective system instructions going into this
312
+ // Responses+tools call. This helps verify assistant + MCP instructions
313
+ // wiring across prompts, assists, autofill, and widget chat.
314
+ try{
315
+ coloredLog('runWithTools systemText:\n' + systemText);
316
+ }catch(_e){}
317
+
318
+ // Normalize tools: manual assistant.tools plus optional MCP tools
319
+ let manualTools = null;
276
320
  if (assistant && assistant.tools) {
277
321
  if (Array.isArray(assistant.tools)) {
278
- tools = assistant.tools;
322
+ manualTools = assistant.tools;
279
323
  } else if (typeof assistant.tools === 'string') {
280
324
  try {
281
325
  const parsed = JSON.parse(assistant.tools);
282
326
  if (Array.isArray(parsed)) {
283
- tools = parsed;
327
+ manualTools = parsed;
284
328
  }
285
329
  } catch (e) {
286
330
  console.error('[chatgpt] Failed to parse assistant.tools JSON', e);
287
331
  }
288
332
  }
289
333
  }
290
- // Normalize tool definitions for the Responses API. The assistant UI
291
- // uses the Assistants-style shape ({ type:'function', function:{...} }),
292
- // but Responses expects the name/description/parameters at the top level:
293
- // { type:'function', name:'x', description:'...', parameters:{...} }
294
- if (Array.isArray(tools)) {
295
- tools = tools.map(function (t) {
334
+ // Flatten any Assistants-style function definitions
335
+ if (Array.isArray(manualTools)) {
336
+ manualTools = manualTools.map(function (t) {
296
337
  if (t && t.type === 'function' && t.function && !t.name) {
297
338
  const fn = t.function || {};
298
339
  return {
@@ -306,6 +347,26 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
306
347
  });
307
348
  }
308
349
 
350
+ // Merge manual tools with MCP tools (manual wins on name collisions).
351
+ let tools = null;
352
+ const mergedByName = {};
353
+ const mcp = buildMcpToolsFromConfig(assistant || {});
354
+ const mcpTools = Array.isArray(mcp.tools) ? mcp.tools : null;
355
+
356
+ if (Array.isArray(manualTools) || Array.isArray(mcpTools)) {
357
+ tools = [];
358
+ (manualTools || []).forEach(function(t){
359
+ if (!t) { return; }
360
+ if (t.name) { mergedByName[t.name] = true; }
361
+ tools.push(t);
362
+ });
363
+ (mcpTools || []).forEach(function(t){
364
+ if (!t || !t.name) { return; }
365
+ if (mergedByName[t.name]) { return; }
366
+ tools.push(t);
367
+ });
368
+ }
369
+
309
370
  // No tools configured – do a simple single Responses call.
310
371
  if (!tools) {
311
372
  const resp = await openai.responses.create({
@@ -1096,6 +1157,24 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1096
1157
  return { error: "Prompt not found." };
1097
1158
  }
1098
1159
 
1160
+ // If this prompt run is associated with a JOE ai_assistant, log which
1161
+ // assistant is being used so we can debug "which agent handled this?"
1162
+ try{
1163
+ const aiAssistantId = data.ai_assistant_id || null;
1164
+ if (aiAssistantId) {
1165
+ let asst = null;
1166
+ try{
1167
+ // Prefer explicit ai_assistant schema lookup, then fallback
1168
+ // to a generic get in case datasets are flattened.
1169
+ asst = $J.get(aiAssistantId,'ai_assistant') || $J.get(aiAssistantId);
1170
+ }catch(_e){}
1171
+ const label = asst && (asst.name || asst.title || asst.info || asst._id) || aiAssistantId;
1172
+ coloredLog('[prompt] executeJOEAiPrompt using ai_assistant: '
1173
+ + label + ' [' + aiAssistantId + ']'
1174
+ + ' for prompt: ' + (prompt.name || prompt._id));
1175
+ }
1176
+ }catch(_e){}
1177
+
1099
1178
  let instructions = prompt.instructions || "";
1100
1179
  let finalInstructions=instructions;
1101
1180
  let finalInput='';
@@ -1705,17 +1784,59 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1705
1784
  }
1706
1785
  }
1707
1786
 
1708
- const assistantId = body.assistant_id || convo.assistant_id || null;
1709
- // NOTE: assistantId here is the OpenAI assistant_id, not the JOE cuid.
1710
- // We do NOT pass assistant_id to the Responses API (it is not supported in the
1711
- // version we are using); instead we look up the JOE ai_assistant by assistant_id
1712
- // and inject its configuration (model, instructions, tools) into the request.
1713
- var assistantObj = null;
1714
- if (assistantId && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
1715
- assistantObj = JOE.Data.ai_assistant.find(function (a) {
1716
- return a && a.assistant_id === assistantId;
1787
+ // Resolve the JOE ai_assistant driving this conversation. We support
1788
+ // both the modern flow (ai_assistant_id / convo.assistant, which are
1789
+ // JOE cuid references) and the legacy OpenAI Assistants flow
1790
+ // (assistant_id / convo.assistant_id, which are OpenAI ids).
1791
+ var assistantObj = null;
1792
+ var joeAssistantId = body.ai_assistant_id || convo.assistant || null; // JOE cuid
1793
+ if (joeAssistantId) {
1794
+ try{
1795
+ // Prefer a direct lookup via the ai_assistant schema, but fall
1796
+ // back to scanning the in-memory dataset if needed. In some
1797
+ // server contexts $J.get may not be wired for ai_assistant yet,
1798
+ // while JOE.Data.ai_assistant is available.
1799
+ assistantObj = $J.get(joeAssistantId,'ai_assistant') || $J.get(joeAssistantId) || null;
1800
+ }catch(_e){}
1801
+ if (!assistantObj && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
1802
+ assistantObj = JOE.Data.ai_assistant.find(function(a){
1803
+ return a && (a._id === joeAssistantId);
1717
1804
  }) || null;
1718
1805
  }
1806
+ }
1807
+ const assistantId = body.assistant_id || convo.assistant_id || (assistantObj && assistantObj.assistant_id) || null;
1808
+ // Legacy fallback: if we only have an OpenAI assistant_id, try to
1809
+ // locate the JOE ai_assistant by that id.
1810
+ if (!assistantObj && assistantId && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
1811
+ assistantObj = JOE.Data.ai_assistant.find(function (a) {
1812
+ return a && a.assistant_id === assistantId;
1813
+ }) || null;
1814
+ }
1815
+
1816
+ // Log which ai_assistant (if any) is being used for this widget
1817
+ // conversation so we can easily confirm the active agent when
1818
+ // debugging object chat vs AI Hub behavior.
1819
+ try{
1820
+ coloredLog('[widget] assistant resolution: '
1821
+ + 'convo.assistant=' + String(convo.assistant || '')
1822
+ + ' convo.assistant_id=' + String(convo.assistant_id || '')
1823
+ + ' body.ai_assistant_id=' + String(body.ai_assistant_id || '')
1824
+ + ' body.assistant_id=' + String(body.assistant_id || '')
1825
+ + ' resolvedJoe=' + String(assistantObj && assistantObj._id || ''));
1826
+ if (assistantObj) {
1827
+ var asstLabel = assistantObj.name || assistantObj.title || assistantObj.info || assistantObj._id || assistantId;
1828
+ coloredLog('[widget] widgetMessage using ai_assistant: '
1829
+ + asstLabel + ' [' + (assistantObj._id || assistantId || '') + ']'
1830
+ + ' source=' + String(convo.source || 'widget')
1831
+ + ' convo=' + String(convo._id || ''));
1832
+ } else if (assistantId) {
1833
+ coloredLog('[widget] widgetMessage assistant_id (no JOE ai_assistant found): '
1834
+ + assistantId
1835
+ + ' source=' + String(convo.source || 'widget')
1836
+ + ' convo=' + String(convo._id || ''));
1837
+ }
1838
+ }catch(_e){}
1839
+
1719
1840
  const openai = newClient();
1720
1841
  const model = (assistantObj && assistantObj.ai_model) || convo.model || body.model || "gpt-5.1";
1721
1842
 
@@ -1741,6 +1862,33 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1741
1862
  }
1742
1863
  }catch(_e){ /* non-fatal */ }
1743
1864
 
1865
+ // Append MCP tool instructions for assistants that have MCP enabled,
1866
+ // using the same helper as other MCP-aware surfaces.
1867
+ try{
1868
+ if (assistantObj && assistantObj.mcp_enabled) {
1869
+ const mcpCfg = {
1870
+ mcp_enabled: assistantObj.mcp_enabled,
1871
+ mcp_toolset: assistantObj.mcp_toolset,
1872
+ mcp_selected_tools: assistantObj.mcp_selected_tools,
1873
+ mcp_instructions_mode: assistantObj.mcp_instructions_mode || 'auto'
1874
+ };
1875
+ const mcp = buildMcpToolsFromConfig(mcpCfg);
1876
+ coloredLog('[widget] MCP config for assistant '
1877
+ + String(assistantObj._id || '') + ': enabled=' + String(mcpCfg.mcp_enabled)
1878
+ + ' toolset=' + String(mcpCfg.mcp_toolset || '')
1879
+ + ' names=' + JSON.stringify(mcp.names || []));
1880
+ if (mcp.names && mcp.names.length) {
1881
+ const txt = MCP.buildToolInstructions(mcp.names, mcpCfg.mcp_instructions_mode || 'auto');
1882
+ if (txt) {
1883
+ coloredLog('[widget] appending MCP instructions block to systemText');
1884
+ systemText = (systemText || '') + '\n\n' + txt;
1885
+ }
1886
+ }
1887
+ } else {
1888
+ coloredLog('[widget] MCP disabled for this assistant or assistant missing; no MCP block appended.');
1889
+ }
1890
+ }catch(_e){ /* non-fatal */ }
1891
+
1744
1892
  // Build the messages array for the model. We deliberately separate
1745
1893
  // the stored `convo.messages` from the model-facing payload so we
1746
1894
  // can annotate the latest user turn with uploaded_files metadata
@@ -335,6 +335,104 @@ function FormBuilder(){
335
335
  }
336
336
  }//end bodycontent
337
337
 
338
+ /**
339
+ * Get form definition JSON from an include
340
+ * GET /API/plugin/formBuilder/definition?include_id={id}
341
+ * GET /API/plugin/formBuilder/definition?formid={form_id}&field=json_definition_include
342
+ * GET /API/plugin/formBuilder/definition?formid={form_id}&pageid={page_id} (auto-finds JSON from page includes)
343
+ */
344
+ this.definition = function(data, req, res) {
345
+ var includeId = data.include_id;
346
+ var formId = data.formid;
347
+ var pageId = data.pageid;
348
+ var includeField = data.field || 'json_definition_include';
349
+
350
+ // If formid provided, look up the include from form's meta, page includes, or specified field
351
+ if (formId && !includeId) {
352
+ var form = JOE.Cache.findByID('form', formId);
353
+ if (!form) {
354
+ return res.status(404).json({errors: 'form not found', failedat: 'formBuilder'});
355
+ }
356
+
357
+ // First try: Check if form has a reference to include in meta or a direct field
358
+ if (form.meta && form.meta[includeField]) {
359
+ includeId = form.meta[includeField];
360
+ } else if (form[includeField]) {
361
+ includeId = form[includeField];
362
+ }
363
+ // Second try: If pageid provided, find JSON include from page's includes
364
+ else if (pageId) {
365
+ var page = JOE.Cache.findByID('page', pageId);
366
+ if (page && page.includes && Array.isArray(page.includes)) {
367
+ for (var i = 0; i < page.includes.length; i++) {
368
+ var inc = JOE.Cache.findByID('include', page.includes[i]);
369
+ if (inc && inc.filetype === 'json') {
370
+ includeId = inc._id;
371
+ break;
372
+ }
373
+ }
374
+ }
375
+ }
376
+
377
+ if (!includeId) {
378
+ return res.status(404).json({errors: 'form does not have json definition include reference. Set it in form.meta.json_definition_include or add a JSON include to the page', failedat: 'formBuilder'});
379
+ }
380
+ }
381
+
382
+ if (!includeId) {
383
+ return res.status(400).json({errors: 'include_id or formid required', failedat: 'formBuilder'});
384
+ }
385
+
386
+ var include = JOE.Cache.findByID('include', includeId);
387
+ if (!include) {
388
+ return res.status(404).json({errors: 'include not found', failedat: 'formBuilder'});
389
+ }
390
+
391
+ if (include.filetype !== 'json') {
392
+ return res.status(400).json({errors: 'include is not a JSON file', failedat: 'formBuilder'});
393
+ }
394
+
395
+ var content = include.content;
396
+
397
+ // If fillTemplate is enabled, process template variables
398
+ if (include.fillTemplate) {
399
+ var payload = {
400
+ WEBCONFIG: JOE.webconfig,
401
+ SETTING: JOE.Cache.settings,
402
+ REQUEST: req,
403
+ FORM: formId ? JOE.Cache.findByID('form', formId) : null
404
+ };
405
+ try {
406
+ var fillTemplate = require('../modules/Sites.js').fillTemplate || function(str, data) {
407
+ return str.replace(/\$\{this\.([^}]+)\}/g, function(match, path) {
408
+ var parts = path.split('.');
409
+ var val = data;
410
+ for (var i = 0; i < parts.length; i++) {
411
+ if (val && typeof val === 'object') {
412
+ val = val[parts[i]];
413
+ } else {
414
+ return match;
415
+ }
416
+ }
417
+ return val != null ? val : match;
418
+ });
419
+ };
420
+ content = fillTemplate(content, payload);
421
+ } catch(e) {
422
+ console.error('[formBuilder.definition] template fill error:', e);
423
+ }
424
+ }
425
+
426
+ // Parse and return JSON
427
+ try {
428
+ var jsonData = typeof content === 'string' ? JSON.parse(content) : content;
429
+ res.set('Content-Type', 'application/json');
430
+ return res.json(jsonData);
431
+ } catch(e) {
432
+ return res.status(500).json({errors: 'invalid JSON: ' + e.message, failedat: 'formBuilder'});
433
+ }
434
+ };
435
+
338
436
  return self;
339
437
  }
340
438
  module.exports = new FormBuilder();
@@ -42,6 +42,11 @@ var schema = {
42
42
  { name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' },
43
43
  { name:'status', type:'string', isReference:true, targetSchema:'status' },
44
44
  { name:'last_synced', type:'string', format:'date-time' },
45
+ // MCP configuration for assistants (mirrors ai_prompt / ai_response)
46
+ { name:'mcp_enabled', type:'boolean' },
47
+ { name:'mcp_toolset', type:'string' },
48
+ { name:'mcp_selected_tools', type:'string', isArray:true },
49
+ { name:'mcp_instructions_mode', type:'string' },
45
50
  { name:'joeUpdated', type:'string', format:'date-time' },
46
51
  { name:'created', type:'string', format:'date-time' }
47
52
  ]
@@ -64,6 +69,7 @@ var schema = {
64
69
  (new Set(schemas)).map(function(schema){
65
70
  subs.push({name:schema,filter:{datasets:{$in:[schema]}}})
66
71
  });
72
+ subs = subs.concat({name:'mcp_enabled',filter:{mcp_enabled:true}});
67
73
  return subs;
68
74
  },
69
75
  stripeColor: function(ai_assistant) {
@@ -353,30 +359,35 @@ var schema = {
353
359
  "itemtype",
354
360
  { section_end: "system" },
355
361
  { sidebar_start: "right", collapsed: false },
356
- "tags",
357
- "status",
358
- {
359
- name: "last_synced",
360
- type: "date",
361
- display: "Last Synced",
362
- locked:true,
363
- comment: "Last time this Assistant was updated at OpenAI."
364
- },
365
- {
366
- name: "sync_button",
367
- type: "button",
368
- display: "Sync to OpenAI",
369
- method: "syncAssistantToOpenAI",
370
- color: "green",
371
- title: "Sync this Assistant with OpenAI"
372
- },
373
- {
374
- name: "set_default_assistant",
375
- type: "button",
376
- display: "Set as Default Assistant",
377
- method: "setAsDefaultAssistant",
378
- color: "orange",
379
- hidden:function(ai_assistant){
362
+ { section_start:'workflow' },
363
+ "status",
364
+ "tags",
365
+ "last_synced",
366
+ {
367
+ name: "sync_button",
368
+ type: "button",
369
+ display: "Sync to OpenAI",
370
+ method: "syncAssistantToOpenAI",
371
+ color: "green",
372
+ title: "Sync this Assistant with OpenAI"
373
+ },
374
+ {
375
+ name: "set_default_assistant",
376
+ type: "button",
377
+ display: "Set as Default Assistant",
378
+ method: "setAsDefaultAssistant",
379
+ color: "orange",
380
+ hidden:function(ai_assistant){
381
+ try{
382
+ var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
383
+ var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
384
+ return !!(existing && existing.value === ai_assistant._id);
385
+ }catch(e){
386
+ return false;
387
+ }
388
+ },
389
+ title: "Mark this assistant as the DEFAULT_AI_ASSISTANT setting.",
390
+ locked:function(ai_assistant){
380
391
  try{
381
392
  var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
382
393
  var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
@@ -384,27 +395,15 @@ var schema = {
384
395
  }catch(e){
385
396
  return false;
386
397
  }
387
- },
388
- title: "Mark this assistant as the DEFAULT_AI_ASSISTANT setting.",
389
- locked:function(ai_assistant){
390
- try{
391
- var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
392
- var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
393
- return !!(existing && existing.value === ai_assistant._id);
394
- }catch(e){
395
- return false;
396
398
  }
397
- }
398
- },
399
- // {
400
- // name: "test_button",
401
- // type: "button",
402
- // display: "Test Button",
403
- // method: "testButtonClick", // <- the method name you want to call
404
- // color: "orange", // <- Joe color class (joe-orange-button)
405
- // title: "Click to test button functionality",
406
- // schema: "ai_assistant" // <- schema to pull the method from (optional for now, we lock it anyway)
407
- // },
399
+ },
400
+ { section_end:'workflow' },
401
+ { section_start:'mcp', display:'MCP Tools', collapsed:true },
402
+ 'mcp_enabled',
403
+ 'mcp_toolset',
404
+ 'mcp_selected_tools',
405
+ 'mcp_instructions_mode',
406
+ { section_end:'mcp' },
408
407
  { sidebar_end: "right" },
409
408
 
410
409
  ];