@plutojl/mcp 0.2.0 → 0.4.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 (2) hide show
  1. package/dist/cli.cjs +347 -61
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -31779,7 +31779,7 @@ var require_cookie = __commonJS({
31779
31779
  "../../node_modules/cookie/index.js"(exports2) {
31780
31780
  "use strict";
31781
31781
  exports2.parse = parse6;
31782
- exports2.serialize = serialize;
31782
+ exports2.serialize = serialize2;
31783
31783
  var __toString = Object.prototype.toString;
31784
31784
  var __hasOwnProperty = Object.prototype.hasOwnProperty;
31785
31785
  var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
@@ -31838,7 +31838,7 @@ var require_cookie = __commonJS({
31838
31838
  }
31839
31839
  return min;
31840
31840
  }
31841
- function serialize(name, val, opt) {
31841
+ function serialize2(name, val, opt) {
31842
31842
  var enc = opt && opt.encode || encodeURIComponent;
31843
31843
  if (typeof enc !== "function") {
31844
31844
  throw new TypeError("option encode is invalid");
@@ -33743,7 +33743,7 @@ var require_uri_all = __commonJS({
33743
33743
  }
33744
33744
  return output.join("");
33745
33745
  }
33746
- function serialize(components) {
33746
+ function serialize2(components) {
33747
33747
  var options = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {};
33748
33748
  var protocol = options.iri ? IRI_PROTOCOL : URI_PROTOCOL;
33749
33749
  var uriTokens = [];
@@ -33799,8 +33799,8 @@ var require_uri_all = __commonJS({
33799
33799
  var skipNormalization = arguments[3];
33800
33800
  var target = {};
33801
33801
  if (!skipNormalization) {
33802
- base2 = parse6(serialize(base2, options), options);
33803
- relative = parse6(serialize(relative, options), options);
33802
+ base2 = parse6(serialize2(base2, options), options);
33803
+ relative = parse6(serialize2(relative, options), options);
33804
33804
  }
33805
33805
  options = options || {};
33806
33806
  if (!options.tolerant && relative.scheme) {
@@ -33851,26 +33851,26 @@ var require_uri_all = __commonJS({
33851
33851
  }
33852
33852
  function resolve(baseURI, relativeURI, options) {
33853
33853
  var schemelessOptions = assign2({ scheme: "null" }, options);
33854
- return serialize(resolveComponents(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true), schemelessOptions);
33854
+ return serialize2(resolveComponents(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true), schemelessOptions);
33855
33855
  }
33856
33856
  function normalize(uri, options) {
33857
33857
  if (typeof uri === "string") {
33858
- uri = serialize(parse6(uri, options), options);
33858
+ uri = serialize2(parse6(uri, options), options);
33859
33859
  } else if (typeOf(uri) === "object") {
33860
- uri = parse6(serialize(uri, options), options);
33860
+ uri = parse6(serialize2(uri, options), options);
33861
33861
  }
33862
33862
  return uri;
33863
33863
  }
33864
33864
  function equal(uriA, uriB, options) {
33865
33865
  if (typeof uriA === "string") {
33866
- uriA = serialize(parse6(uriA, options), options);
33866
+ uriA = serialize2(parse6(uriA, options), options);
33867
33867
  } else if (typeOf(uriA) === "object") {
33868
- uriA = serialize(uriA, options);
33868
+ uriA = serialize2(uriA, options);
33869
33869
  }
33870
33870
  if (typeof uriB === "string") {
33871
- uriB = serialize(parse6(uriB, options), options);
33871
+ uriB = serialize2(parse6(uriB, options), options);
33872
33872
  } else if (typeOf(uriB) === "object") {
33873
- uriB = serialize(uriB, options);
33873
+ uriB = serialize2(uriB, options);
33874
33874
  }
33875
33875
  return uriA === uriB;
33876
33876
  }
@@ -33889,7 +33889,7 @@ var require_uri_all = __commonJS({
33889
33889
  }
33890
33890
  return components;
33891
33891
  },
33892
- serialize: function serialize2(components, options) {
33892
+ serialize: function serialize3(components, options) {
33893
33893
  var secure = String(components.scheme).toLowerCase() === "https";
33894
33894
  if (components.port === (secure ? 443 : 80) || components.port === "") {
33895
33895
  components.port = void 0;
@@ -33920,7 +33920,7 @@ var require_uri_all = __commonJS({
33920
33920
  wsComponents.query = void 0;
33921
33921
  return wsComponents;
33922
33922
  },
33923
- serialize: function serialize2(wsComponents, options) {
33923
+ serialize: function serialize3(wsComponents, options) {
33924
33924
  if (wsComponents.port === (isSecure(wsComponents) ? 443 : 80) || wsComponents.port === "") {
33925
33925
  wsComponents.port = void 0;
33926
33926
  }
@@ -34094,7 +34094,7 @@ var require_uri_all = __commonJS({
34094
34094
  }
34095
34095
  return uuidComponents;
34096
34096
  },
34097
- serialize: function serialize2(uuidComponents, options) {
34097
+ serialize: function serialize3(uuidComponents, options) {
34098
34098
  var urnComponents = uuidComponents;
34099
34099
  urnComponents.nss = (uuidComponents.uuid || "").toLowerCase();
34100
34100
  return urnComponents;
@@ -34112,7 +34112,7 @@ var require_uri_all = __commonJS({
34112
34112
  exports3.pctDecChars = pctDecChars;
34113
34113
  exports3.parse = parse6;
34114
34114
  exports3.removeDotSegments = removeDotSegments;
34115
- exports3.serialize = serialize;
34115
+ exports3.serialize = serialize2;
34116
34116
  exports3.resolveComponents = resolveComponents;
34117
34117
  exports3.resolve = resolve;
34118
34118
  exports3.normalize = normalize;
@@ -39140,8 +39140,8 @@ var require_ajv = __commonJS({
39140
39140
  delete this._refs[schemaKeyRef];
39141
39141
  return this;
39142
39142
  case "object":
39143
- var serialize = this._opts.serialize;
39144
- var cacheKey = serialize ? serialize(schemaKeyRef) : schemaKeyRef;
39143
+ var serialize2 = this._opts.serialize;
39144
+ var cacheKey = serialize2 ? serialize2(schemaKeyRef) : schemaKeyRef;
39145
39145
  this._cache.del(cacheKey);
39146
39146
  var id = this._getId(schemaKeyRef);
39147
39147
  if (id) {
@@ -39164,8 +39164,8 @@ var require_ajv = __commonJS({
39164
39164
  function _addSchema(schema, skipValidation, meta, shouldAddSchema) {
39165
39165
  if (typeof schema != "object" && typeof schema != "boolean")
39166
39166
  throw new Error("schema should be object or boolean");
39167
- var serialize = this._opts.serialize;
39168
- var cacheKey = serialize ? serialize(schema) : schema;
39167
+ var serialize2 = this._opts.serialize;
39168
+ var cacheKey = serialize2 ? serialize2(schema) : schema;
39169
39169
  var cached = this._cache.get(cacheKey);
39170
39170
  if (cached) return cached;
39171
39171
  shouldAddSchema = shouldAddSchema || this._opts.addUsedSchema !== false;
@@ -39349,6 +39349,7 @@ Run options:
39349
39349
  --pluto-port <port> Pluto server port (default: ${DEFAULTS.plutoPort})
39350
39350
  --pluto-url <url> Connect to existing Pluto server (skip starting one)
39351
39351
  --julia-version <ver> Julia version via juliaup (default: ${DEFAULTS.juliaVersion})
39352
+ --no-pluto Start MCP server only, without starting Pluto
39352
39353
 
39353
39354
  Install options:
39354
39355
  --target <target> Config target: claude-code, copilot, all (default: claude-code)
@@ -39440,6 +39441,8 @@ function parseArgs(argv) {
39440
39441
  args.force = true;
39441
39442
  } else if (flag === "--raw") {
39442
39443
  args.raw = true;
39444
+ } else if (flag === "--no-pluto") {
39445
+ args.noPluto = true;
39443
39446
  } else {
39444
39447
  console.warn(`Unknown flag: ${flag}`);
39445
39448
  }
@@ -39478,7 +39481,8 @@ function resolveRunConfig(args) {
39478
39481
  plutoPort: args.plutoPort ?? envInt("PLUTO_PORT") ?? file.plutoPort ?? DEFAULTS.plutoPort,
39479
39482
  plutoUrl: args.plutoUrl ?? process.env.PLUTO_SERVER_URL ?? file.serverUrl ?? void 0,
39480
39483
  juliaVersion: args.juliaVersion ?? process.env.JULIA_VERSION ?? file.juliaVersion ?? DEFAULTS.juliaVersion,
39481
- workDir
39484
+ workDir,
39485
+ noPluto: args.noPluto ?? false
39482
39486
  };
39483
39487
  }
39484
39488
  function resolveInstallArgs(args) {
@@ -54678,6 +54682,116 @@ end`,
54678
54682
  });
54679
54683
  }
54680
54684
  };
54685
+ var NOTEBOOK_HEADER = "### A Pluto.jl notebook ###";
54686
+ var CELL_ID_DELIMITER = "# \u2554\u2550\u2561 ";
54687
+ var CELL_METADATA_PREFIX = "# \u2560\u2550\u2561 ";
54688
+ var ORDER_DELIMITER = "# \u2560\u2550";
54689
+ var ORDER_DELIMITER_FOLDED = "# \u255F\u2500";
54690
+ var DISABLED_PREFIX = "#=\u2560\u2550\u2561\n";
54691
+ var DISABLED_SUFFIX = "\n \u2560\u2550\u2561 =#";
54692
+ var PTOML_CELL_ID = "00000000-0000-0000-0000-000000000001";
54693
+ var MTOML_CELL_ID$1 = "00000000-0000-0000-0000-000000000002";
54694
+ var DEFAULT_CELL_METADATA = {
54695
+ disabled: false,
54696
+ show_logs: true,
54697
+ skip_as_script: false
54698
+ };
54699
+ function generateBindMacro() {
54700
+ return [
54701
+ "",
54702
+ "# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).",
54703
+ "macro bind(def, element)",
54704
+ " quote",
54705
+ ' local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end',
54706
+ " local el = $(esc(element))",
54707
+ " global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)",
54708
+ " el",
54709
+ " end",
54710
+ "end"
54711
+ ];
54712
+ }
54713
+ function serializeCellMetadata(metadata) {
54714
+ const lines = [];
54715
+ if (metadata.disabled && !metadata._implicit_disabled) {
54716
+ lines.push(CELL_METADATA_PREFIX + "disabled = true");
54717
+ }
54718
+ if (metadata.show_logs !== void 0 && metadata.show_logs !== DEFAULT_CELL_METADATA.show_logs) {
54719
+ lines.push(CELL_METADATA_PREFIX + `show_logs = ${metadata.show_logs}`);
54720
+ }
54721
+ if (metadata.skip_as_script !== void 0 && metadata.skip_as_script !== DEFAULT_CELL_METADATA.skip_as_script) {
54722
+ lines.push(
54723
+ CELL_METADATA_PREFIX + `skip_as_script = ${metadata.skip_as_script}`
54724
+ );
54725
+ }
54726
+ return [
54727
+ ...lines,
54728
+ ...Object.entries(metadata).filter(([name, entry]) => {
54729
+ return !["skip_as_script", "show_logs", "disabled"].includes(name);
54730
+ }).map(
54731
+ ([name, entry]) => `${CELL_METADATA_PREFIX}${name} = ${JSON.stringify(entry)}`
54732
+ )
54733
+ ];
54734
+ }
54735
+ function serialize(notebookData) {
54736
+ const lines = [];
54737
+ lines.push(NOTEBOOK_HEADER);
54738
+ lines.push(`# ${notebookData.pluto_version || "v0.20.10"}`);
54739
+ lines.push("");
54740
+ if (notebookData.metadata && notebookData.metadata._raw_metadata_lines) {
54741
+ for (const metadataLine of notebookData.metadata._raw_metadata_lines) {
54742
+ lines.push("#> " + metadataLine);
54743
+ }
54744
+ lines.push("");
54745
+ }
54746
+ lines.push("using Markdown");
54747
+ lines.push("using InteractiveUtils");
54748
+ if (notebookData._has_bind_macro) {
54749
+ lines.push(...generateBindMacro());
54750
+ }
54751
+ lines.push("");
54752
+ const topologicalOrder = notebookData._topological_order || notebookData.cell_order || [];
54753
+ const cellInputs = notebookData.cell_inputs || {};
54754
+ for (const cellId of topologicalOrder) {
54755
+ const cellInput = cellInputs[cellId];
54756
+ if (!cellInput) continue;
54757
+ lines.push(CELL_ID_DELIMITER + cellId);
54758
+ const metadata = cellInput.metadata || {};
54759
+ const metadataLines = serializeCellMetadata(metadata);
54760
+ lines.push(...metadataLines);
54761
+ let code = cellInput.code || "";
54762
+ if (metadata.disabled && !code.startsWith(DISABLED_PREFIX)) {
54763
+ code = DISABLED_PREFIX + code + DISABLED_SUFFIX;
54764
+ }
54765
+ lines.push(code);
54766
+ lines.push("");
54767
+ }
54768
+ const packageCells = notebookData._package_cells || {};
54769
+ if (packageCells[PTOML_CELL_ID]) {
54770
+ lines.push(CELL_ID_DELIMITER + PTOML_CELL_ID);
54771
+ lines.push(packageCells[PTOML_CELL_ID]);
54772
+ lines.push("");
54773
+ }
54774
+ if (packageCells[MTOML_CELL_ID$1]) {
54775
+ lines.push(CELL_ID_DELIMITER + MTOML_CELL_ID$1);
54776
+ lines.push(packageCells[MTOML_CELL_ID$1]);
54777
+ lines.push("");
54778
+ }
54779
+ lines.push(CELL_ID_DELIMITER + "Cell order:");
54780
+ const cellOrder = notebookData.cell_order || [];
54781
+ for (const cellId of cellOrder) {
54782
+ const cellInput = cellInputs[cellId];
54783
+ if (!cellInput) continue;
54784
+ const delimiter = cellInput.code_folded ? ORDER_DELIMITER_FOLDED : ORDER_DELIMITER;
54785
+ lines.push(delimiter + cellId);
54786
+ }
54787
+ if (packageCells[PTOML_CELL_ID]) {
54788
+ lines.push(ORDER_DELIMITER_FOLDED + PTOML_CELL_ID);
54789
+ }
54790
+ if (packageCells[MTOML_CELL_ID$1]) {
54791
+ lines.push(ORDER_DELIMITER_FOLDED + MTOML_CELL_ID$1);
54792
+ }
54793
+ return lines.join("\n") + "\n";
54794
+ }
54681
54795
 
54682
54796
  // ../../src/plutoManager.ts
54683
54797
  var import_events = require("events");
@@ -54949,6 +55063,16 @@ var PlutoManager = class {
54949
55063
  await worker2.deleteSnippets([result.cell_id]);
54950
55064
  return result;
54951
55065
  }
55066
+ /**
55067
+ * Get the serialized notebook content (.jl format) for saving to disk
55068
+ */
55069
+ getNotebookContent(worker2) {
55070
+ const state = worker2.getState();
55071
+ if (!state) {
55072
+ throw new Error("Notebook state not available");
55073
+ }
55074
+ return serialize(state);
55075
+ }
54952
55076
  /**
54953
55077
  * Close all notebook connections
54954
55078
  */
@@ -54984,6 +55108,7 @@ var PlutoManager = class {
54984
55108
 
54985
55109
  // ../../src/mcp-server-http.ts
54986
55110
  var import_express = __toESM(require_express2(), 1);
55111
+ var import_promises = require("fs/promises");
54987
55112
 
54988
55113
  // ../../node_modules/zod/v3/external.js
54989
55114
  var external_exports = {};
@@ -62666,7 +62791,7 @@ data: ${JSON.stringify(message)}
62666
62791
  };
62667
62792
 
62668
62793
  // ../../src/PLUTO_GUIDE.md
62669
- var PLUTO_GUIDE_default = '# Pluto.jl Notebook Guide for AI Tools\n\nThis guide explains how to work with Pluto.jl notebooks, including cell structure, reactivity rules, PlutoUI components, and best practices.\n\n## Table of Contents\n\n- [Notebook Structure](#notebook-structure)\n- [Cell Rules and Reactivity](#cell-rules-and-reactivity)\n- [Markdown Cells](#markdown-cells)\n- [PlutoUI Components](#plutoui-components)\n- [Combining Markdown and PlutoUI](#combining-markdown-and-plutoui)\n- [Best Practices](#best-practices)\n- [Common Patterns](#common-patterns)\n\n---\n\n## Notebook Structure\n\n### File Format\n\nA Pluto notebook is a Julia file (`.jl`) with a specific structure:\n\n```julia\n### A Pluto.jl notebook ###\n# v0.20.19\n\nusing Markdown\nusing InteractiveUtils\n\n# \u2554\u2550\u2561 cell-uuid-1\n# \u2560\u2550\u2561 disabled = false\n# \u2560\u2550\u2561 show_logs = true\n# \u2560\u2550\u2561 skip_as_script = false\nmd"""\n# Your content here\n"""\n\n# \u2554\u2550\u2561 cell-uuid-2\nx = 10\n\n# \u2554\u2550\u2561 Cell order:\n# \u2560\u2550cell-uuid-1\n# \u2560\u2550cell-uuid-2\n```\n\n### Cell Markers\n\nEach cell starts with a unique identifier:\n\n- `# \u2554\u2550\u2561 <uuid>` - Cell boundary marker\n- Cell UUIDs are automatically generated\n- Cells contain Julia code, markdown, or expressions\n\n### Cell Metadata\n\nCells can have metadata comments:\n\n- `# \u2560\u2550\u2561 disabled = false` - Cell execution state\n- `# \u2560\u2550\u2561 show_logs = true` - Show cell output logs\n- `# \u2560\u2550\u2561 skip_as_script = false` - Include cell when exporting as script\n\n### Cell Order Section\n\nAt the end of the notebook:\n\n```julia\n# \u2554\u2550\u2561 Cell order:\n# \u2560\u2550cell-uuid-1\n# \u2560\u2550cell-uuid-2\n# \u2560\u2550cell-uuid-3\n```\n\nThis defines the display order of cells (not execution order).\n\n---\n\n## Cell Rules and Reactivity\n\n### Reactive Execution Model\n\n**Key Principle**: Pluto automatically determines execution order based on variable dependencies.\n\n#### Rules\n\n1. **One Variable per Cell (Assignment)**\n\n ```julia\n # \u2705 CORRECT\n x = 10\n\n # \u274C WRONG - Cannot assign same variable in another cell\n x = 20 # Error: Multiple definitions of x\n ```\n\n2. **Use `begin...end` for Multiple Statements**\n\n ```julia\n # \u2705 CORRECT\n begin\n x = 10\n y = 20\n z = x + y\n end\n ```\n\n3. **Automatic Dependency Tracking**\n\n ```julia\n # Cell 1\n a = 5\n\n # Cell 2 (depends on Cell 1)\n b = a * 2 # Automatically re-runs when \'a\' changes\n\n # Cell 3 (depends on Cell 2)\n c = b + 10 # Automatically re-runs when \'b\' changes\n ```\n\n4. **No Hidden State**\n - Every variable is defined exactly once\n - Execution order is determined by dependencies, not cell order\n - Deleting a cell removes its variables completely\n\n5. **Import/Package Management**\n\n ```julia\n begin\n import Pkg\n Pkg.activate(; temp=true)\n Pkg.add(["Plots", "DataFrames", "PlutoUI"])\n using Plots\n using PlutoUI\n end\n ```\n\n---\n\n## Markdown Cells\n\n### Basic Markdown Syntax\n\nMarkdown cells use triple-quote syntax:\n\n```julia\nmd"""\n# Heading 1\n## Heading 2\n### Heading 3\n\n**Bold text**\n*Italic text*\n\n- Bullet point 1\n- Bullet point 2\n\n1. Numbered item 1\n2. Numbered item 2\n\n[Link text](https://example.com)\n"""\n```\n\n### LaTeX Math\n\n```julia\nmd"""\nInline math: $E = mc^2$\n\nDisplay math:\n$$\n\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}\n$$\n"""\n```\n\n### Interpolating Variables\n\nUse `$()` to embed Julia expressions:\n\n```julia\nx = 42\n\nmd"""\nThe value of x is $(x).\n\nComputed value: $(x * 2)\n"""\n```\n\n### Tables in Markdown\n\n```julia\nmd"""\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Value A | Value B | Value C |\n| Value D | Value E | Value F |\n"""\n```\n\n---\n\n## PlutoUI Components\n\nPlutoUI provides interactive widgets using the `@bind` macro.\n\n### Installation\n\n```julia\nbegin\n import Pkg\n Pkg.add("PlutoUI")\n using PlutoUI\nend\n```\n\n### @bind Macro\n\nThe `@bind` macro connects a UI element to a variable:\n\n```julia\n@bind variable_name Widget(options...)\n```\n\n### Available Components\n\n#### 1. Slider\n\nCreates a slider for numeric input.\n\n```julia\n# Basic slider\n@bind x Slider(1:100)\n\n# Slider with default value and display\n@bind temperature Slider(0:0.1:100, default=25, show_value=true)\n\n# Slider with custom range\n@bind alpha Slider(0.0:0.01:1.0, default=0.5)\n```\n\n**Parameters:**\n\n- `range` - Range of values (e.g., `1:10` or `0.0:0.1:1.0`)\n- `default` - Initial value\n- `show_value` - Display current value (true/false)\n\n#### 2. TextField\n\nText input field.\n\n```julia\n# Single-line text field\n@bind name TextField()\n\n# Text field with default value\n@bind description TextField(default="Enter description")\n\n# Multi-line textarea\n@bind notes TextField((50, 10)) # (cols, rows)\n```\n\n**Parameters:**\n\n- `default` - Initial text value\n- `dims=(cols, rows)` - For multi-line textarea\n\n#### 3. NumberField\n\nNumeric input field.\n\n```julia\n# Number field with range\n@bind count NumberField(1:100, default=10)\n\n# Float number field\n@bind value NumberField(0.0:0.1:10.0, default=5.0)\n```\n\n**Parameters:**\n\n- `range` - Valid number range\n- `default` - Initial value\n\n#### 4. CheckBox\n\nBoolean checkbox.\n\n```julia\n# Simple checkbox\n@bind enabled CheckBox()\n\n# Checkbox with default value\n@bind show_details CheckBox(default=true)\n```\n\n**Parameters:**\n\n- `default` - Initial state (true/false)\n\n#### 5. Select\n\nDropdown selection.\n\n```julia\n# Select from array\n@bind color Select(["red", "green", "blue"])\n\n# Select with default\n@bind option Select(["Option A", "Option B", "Option C"], default="Option B")\n\n# Select from pairs (value => label)\n@bind choice Select([1 => "First", 2 => "Second", 3 => "Third"])\n```\n\n**Parameters:**\n\n- `options` - Array of options or pairs\n- `default` - Initially selected value\n\n#### 6. MultiSelect\n\nMultiple selection dropdown.\n\n```julia\n# Multi-select\n@bind colors MultiSelect(["red", "green", "blue", "yellow"])\n\n# Multi-select with defaults\n@bind selected MultiSelect(\n ["A", "B", "C", "D"],\n default=["A", "C"]\n)\n```\n\n#### 7. Button\n\nClickable button.\n\n```julia\n# Simple button\n@bind clicked Button("Click me!")\n\n# Button that increments on each click\n@bind click_count Button("Increment")\n```\n\n**Note**: Button sends the same value each time clicked, triggering reactive cells.\n\n#### 8. Radio\n\nRadio button group (single selection).\n\n```julia\n@bind choice Radio(["Option 1", "Option 2", "Option 3"])\n\n@bind size Radio(["Small", "Medium", "Large"], default="Medium")\n```\n\n#### 9. FilePicker\n\nFile upload widget.\n\n```julia\n@bind uploaded_file FilePicker()\n\n# Access file content\nfile_data = uploaded_file["data"]\nfile_name = uploaded_file["name"]\n```\n\n#### 10. Clock\n\nTimer that ticks at regular intervals.\n\n```julia\n# Tick every second\n@bind tick Clock()\n\n# Tick every 0.5 seconds\n@bind tick Clock(0.5)\n```\n\n#### 11. DateField\n\nDate picker.\n\n```julia\n@bind selected_date DateField()\n\n@bind start_date DateField(default=Dates.today())\n```\n\n---\n\n## Combining Markdown and PlutoUI\n\n### Inline Widgets in Tables\n\nYou can embed PlutoUI widgets directly in markdown tables:\n\n```julia\nmd"""\n| Parameter | Description | Units | Value |\n| --------- | ----------- | ----- | ----- |\n| `T_inf` | Ambient temperature | K | $(@bind T_inf NumberField(0:500, default=300)) |\n| `h` | Heat transfer coefficient | W/(m\xB2\xB7K) | $(@bind h Slider(0.0:0.1:1.0, default=0.7)) |\n| `mass` | Mass | kg | $(@bind m NumberField(0.0:0.1:10.0, default=1.0)) |\n"""\n```\n\n### Using Widget Values in Markdown\n\n```julia\n@bind speed Slider(1:100, default=50, show_value=true)\n\nmd"""\n## Speed Control\n\nCurrent speed: $(speed) km/h\n\nStatus: $(speed > 80 ? "\u{1F534} Fast" : "\u{1F7E2} Normal")\n"""\n```\n\n### Embedded Plots and Computations\n\n```julia\nmd"""\n# Wave Parameters\n\n| Parameter | Value |\n| --------- | ----- |\n| Amplitude | $(@bind amplitude Slider(0.1:0.1:5.0, default=1.0, show_value=true)) |\n| Frequency | $(@bind frequency Slider(0.1:0.1:10.0, default=1.0, show_value=true)) |\n\n## Waveform\n\n$(begin\n x = 0:0.01:2\u03C0\n y = amplitude .* sin.(frequency .* x)\n plot(x, y, label="sin wave", xlabel="x", ylabel="y")\nend)\n"""\n```\n\n### Dynamic Content Generation\n\n```julia\nbegin\n @bind param1 NumberField(0:100, default=50)\n @bind param2 Slider(0.0:0.1:1.0, default=0.5)\n\n result = param1 * param2\n\n md"""\n # Interactive Calculator\n\n | Input | Value |\n | ----- | ----- |\n | Parameter 1 | $(param1) |\n | Parameter 2 | $(param2) |\n | **Result** | **$(result)** |\n\n The computation shows: $(param1) \xD7 $(param2) = $(result)\n """\nend\n```\n\n---\n\n## Best Practices\n\n### 1. Package Management\n\nAlways use a cell at the beginning for package management:\n\n```julia\nbegin\n import Pkg\n Pkg.activate(; temp=true)\n Pkg.add(["Plots", "DataFrames", "PlutoUI"])\n using Plots\n using DataFrames\n using PlutoUI\nend\n```\n\n### 2. Organize with Markdown Headers\n\n```julia\nmd"""\n# Section 1: Data Loading\n\nLoad and prepare the data.\n"""\n\n# ... code cells ...\n\nmd"""\n# Section 2: Analysis\n\nPerform the analysis.\n"""\n\n# ... code cells ...\n```\n\n### 3. Use `begin...end` for Complex Logic\n\n```julia\nbegin\n # Multiple related computations\n data = load_data()\n cleaned = clean_data(data)\n result = analyze(cleaned)\n result\nend\n```\n\n### 4. Display the Last Expression\n\nThe last expression in a cell is automatically displayed:\n\n```julia\nbegin\n x = 10\n y = 20\n x + y # This value is displayed\nend\n```\n\n### 5. Suppress Output with Semicolon\n\n```julia\n# Suppress display\nlarge_data = load_large_dataset();\n\n# Show specific output\nprintln("Data loaded successfully")\n```\n\n### 6. Use @doc for Documentation\n\n```julia\nmd"""\n## Function Documentation\n\n$(@doc my_function)\n"""\n```\n\n### 7. Bind Multiple Related Widgets\n\n```julia\nmd"""\n## Configuration\n\n| Parameter | Value |\n| --------- | ----- |\n| X | $(@bind x Slider(1:10, default=5)) |\n| Y | $(@bind y Slider(1:10, default=5)) |\n\nSum: $(x + y)\nProduct: $(x * y)\n"""\n```\n\n---\n\n## Common Patterns\n\n### Pattern 1: Interactive Parameter Sweep\n\n```julia\n# Define parameters with widgets\n@bind param Slider(1:100, default=50, show_value=true)\n\n# Compute based on parameter\nresult = expensive_computation(param)\n\n# Display results\nplot(result)\n```\n\n### Pattern 2: Conditional Display\n\n```julia\n@bind show_advanced CheckBox(default=false)\n```\n\n```julia\n\nmd"""\nShow advanced options: $(show_advanced ? "\u2705 Enabled" : "\u274C Disabled")\n\n$(if show_advanced\n md"## Advanced Settings\n\n Configure advanced parameters here."\nelse\n md""\nend)\n"""\n```\n\n### Pattern 3: Multi-Step Workflow\n\n```julia\nmd"""\n# Step 1: Select Dataset\n$(@bind dataset Select(["Dataset A", "Dataset B", "Dataset C"]))\n"""\n\n# Load selected dataset\ndata = load_dataset(dataset)\n\nmd"""\n# Step 2: Configure Analysis\n$(@bind threshold Slider(0:0.1:1, default=0.5, show_value=true))\n"""\n\n# Perform analysis\nresults = analyze(data, threshold)\n\nmd"""\n# Step 3: Results\n\n$(plot(results))\n"""\n```\n\n### Pattern 4: Table with Embedded Widgets\n\n```julia\nmd"""\n# Parameter Configuration\n\n| Parameter | Description | Units | Value |\n| --------- | ----------- | ----- | ----- |\n| `temperature` | Operating temperature | \xB0C | $(@bind temp NumberField(0:200, default=25)) |\n| `pressure` | Operating pressure | bar | $(@bind pres NumberField(0:100, default=1)) |\n| `enabled` | Enable feature | - | $(@bind enabled CheckBox(default=true)) |\n"""\n```\n\n```julia\n# Use the bound values\nconfig = (temperature=temp, pressure=pres, enabled=enabled)\n```\n\n### Pattern 5: Real-time Visualization\n\n```julia\n@bind time Clock(0.1) # Update every 0.1 seconds\n\nbegin\n # Generate time-varying data\n t = time\n x = 0:0.1:2\u03C0\n y = sin.(x .+ t)\n\n plot(x, y, label="sin(x + t)", ylims=(-1.5, 1.5))\nend\n```\n\n### Pattern 6: Form-like Interface\n\n```julia\nmd"""\n# User Profile\n\n| Field | Input |\n| ----- | ----- |\n| Name | $(@bind user_name TextField(default="")) |\n| Age | $(@bind user_age NumberField(1:120, default=25)) |\n| Country | $(@bind country Select(["USA", "UK", "Canada", "Other"])) |\n| Subscribe | $(@bind subscribe CheckBox(default=false)) |\n\n$(@bind submit_button Button("Submit"))\n"""\n```\n\n```julia\nbegin\n profile = (\n name = user_name,\n age = user_age,\n country = country,\n subscribe = subscribe\n )\n\n md"""\n ## Submitted Profile\n\n - **Name**: $(profile.name)\n - **Age**: $(profile.age)\n - **Country**: $(profile.country)\n - **Newsletter**: $(profile.subscribe ? "\u2705 Yes" : "\u274C No")\n """\nend\n```\n\n---\n\n## Summary\n\n### Key Takeaways\n\n1. **Reactivity**: Cells automatically re-run based on dependencies\n2. **One Variable Rule**: Each variable can only be defined once across all cells\n3. **`@bind` Macro**: Connects UI widgets to variables\n4. **Markdown Integration**: Use `$()` to embed Julia expressions in markdown\n5. **Tables + Widgets**: Combine markdown tables with PlutoUI for interactive forms\n6. **No Hidden State**: All variables are explicit and traceable\n\n### Quick Reference\n\n**Slider**: `@bind x Slider(1:100, default=50, show_value=true)`\n\n**TextField**: `@bind text TextField(default="")`\n\n**NumberField**: `@bind n NumberField(0:100, default=10)`\n\n**CheckBox**: `@bind checked CheckBox(default=false)`\n\n**Select**: `@bind choice Select(["A", "B", "C"])`\n\n**Button**: `@bind clicked Button("Click")`\n\n**Markdown + Widget**: `$(@bind x Slider(1:10))`\n\n**Markdown + Variable**: `The value is $(x)`\n\n---\n\n## Additional Resources\n\n- Official Pluto.jl: <https://plutojl.org/>\n- PlutoUI Documentation: <https://github.com/JuliaPluto/PlutoUI.jl>\n- Featured Examples: <https://featured.plutojl.org/>\n\n---\n\n**Note for AI Tools**: When creating or modifying Pluto notebooks:\n\n- Always respect the one-variable-per-cell rule\n- Use `begin...end` blocks for multiple statements\n- Ensure cell UUIDs are unique\n- Maintain the cell order section at the end\n- Use `@bind` for all interactive widgets\n- Test reactivity by changing dependent variables\n';
62794
+ var PLUTO_GUIDE_default = '# Pluto.jl Notebook Guide for AI Tools\n\nThis guide explains how to work with Pluto.jl notebooks, including cell structure, reactivity rules, PlutoUI components, and best practices.\n\n## Important: Server Startup Times\n\nStarting a Pluto server involves installing Julia packages and precompiling them. **The first run can take several minutes** (2-10 minutes depending on the system). If `start_pluto_server` or `open_notebook` appears to hang or times out:\n\n1. **Do not retry immediately** \u2014 the server is likely still starting up.\n2. **Wait 30-60 seconds**, then call `get_notebook_status` to check progress.\n3. **Subsequent runs are much faster** since packages are cached.\n\nIf a tool call times out, that does NOT mean it failed \u2014 the server may still be starting in the background. Check `get_notebook_status` before retrying.\n\n## Working with Notebooks via MCP\n\nThese rules are critical when interacting with Pluto notebooks through the MCP API:\n\n### File Ownership\n\n- **Never edit the `.pluto.jl` file on disk while the notebook is open** \u2014 Pluto owns that file. Changes made outside the MCP API will be ignored or overwritten.\n- All cell mutations (create, edit, delete) must go through the MCP tools.\n- To persist changes to disk, call `save_notebook` explicitly. **Notebooks are NOT auto-saved.**\n\n### Handling Timeouts\n\n- If `create_cell` times out, the cell was likely still created in Pluto. Use `list_cells` to check before retrying \u2014 creating the same cell twice causes "Multiple definitions" errors.\n- For slow operations (e.g. `import Pkg; Pkg.add(...)`), prefer: (1) `edit_cell` with `run=false` to set the code, then (2) `execute_cell` to run it. This avoids timeout-induced phantom cells.\n- Use `delete_cell` to remove any accidental duplicate cells.\n\n### Pluto Reactivity Rules\n\n- **Each variable can only be defined in one cell.** If you get a "Multiple definitions" error, use `list_cells` to find the duplicate, then `delete_cell` to remove it.\n- When you edit a cell, Pluto automatically re-runs all cells that depend on the changed variables.\n- Prefer creating package cells with `import Pkg; Pkg.activate(; temp=true); Pkg.add([...])` in one cell, and `using PackageName` in a separate cell.\n\n### Recommended Workflow\n\n1. `open_notebook` (file must exist on disk first)\n2. `list_cells` to see current state\n3. `create_cell` / `edit_cell` / `delete_cell` to make changes\n4. `read_cell` to inspect outputs\n5. `save_notebook` to persist to disk when done\n6. `get_notebook_url` to give the user a browser link\n\n## Table of Contents\n\n- [Notebook Structure](#notebook-structure)\n- [Cell Rules and Reactivity](#cell-rules-and-reactivity)\n- [Markdown Cells](#markdown-cells)\n- [PlutoUI Components](#plutoui-components)\n- [Combining Markdown and PlutoUI](#combining-markdown-and-plutoui)\n- [Best Practices](#best-practices)\n- [Common Patterns](#common-patterns)\n\n---\n\n## Notebook Structure\n\n### File Format\n\nA Pluto notebook is a Julia file (`.jl`) with a specific structure:\n\n```julia\n### A Pluto.jl notebook ###\n# v0.20.19\n\nusing Markdown\nusing InteractiveUtils\n\n# \u2554\u2550\u2561 cell-uuid-1\n# \u2560\u2550\u2561 disabled = false\n# \u2560\u2550\u2561 show_logs = true\n# \u2560\u2550\u2561 skip_as_script = false\nmd"""\n# Your content here\n"""\n\n# \u2554\u2550\u2561 cell-uuid-2\nx = 10\n\n# \u2554\u2550\u2561 Cell order:\n# \u2560\u2550cell-uuid-1\n# \u2560\u2550cell-uuid-2\n```\n\n### Cell Markers\n\nEach cell starts with a unique identifier:\n\n- `# \u2554\u2550\u2561 <uuid>` - Cell boundary marker\n- Cell UUIDs are automatically generated\n- Cells contain Julia code, markdown, or expressions\n\n### Cell Metadata\n\nCells can have metadata comments:\n\n- `# \u2560\u2550\u2561 disabled = false` - Cell execution state\n- `# \u2560\u2550\u2561 show_logs = true` - Show cell output logs\n- `# \u2560\u2550\u2561 skip_as_script = false` - Include cell when exporting as script\n\n### Cell Order Section\n\nAt the end of the notebook:\n\n```julia\n# \u2554\u2550\u2561 Cell order:\n# \u2560\u2550cell-uuid-1\n# \u2560\u2550cell-uuid-2\n# \u2560\u2550cell-uuid-3\n```\n\nThis defines the display order of cells (not execution order).\n\n---\n\n## Cell Rules and Reactivity\n\n### Reactive Execution Model\n\n**Key Principle**: Pluto automatically determines execution order based on variable dependencies.\n\n#### Rules\n\n1. **One Variable per Cell (Assignment)**\n\n ```julia\n # \u2705 CORRECT\n x = 10\n\n # \u274C WRONG - Cannot assign same variable in another cell\n x = 20 # Error: Multiple definitions of x\n ```\n\n2. **Use `begin...end` for Multiple Statements**\n\n ```julia\n # \u2705 CORRECT\n begin\n x = 10\n y = 20\n z = x + y\n end\n ```\n\n3. **Automatic Dependency Tracking**\n\n ```julia\n # Cell 1\n a = 5\n\n # Cell 2 (depends on Cell 1)\n b = a * 2 # Automatically re-runs when \'a\' changes\n\n # Cell 3 (depends on Cell 2)\n c = b + 10 # Automatically re-runs when \'b\' changes\n ```\n\n4. **No Hidden State**\n - Every variable is defined exactly once\n - Execution order is determined by dependencies, not cell order\n - Deleting a cell removes its variables completely\n\n5. **Import/Package Management**\n\n ```julia\n begin\n import Pkg\n Pkg.activate(; temp=true)\n Pkg.add(["Plots", "DataFrames", "PlutoUI"])\n using Plots\n using PlutoUI\n end\n ```\n\n---\n\n## Markdown Cells\n\n### Basic Markdown Syntax\n\nMarkdown cells use triple-quote syntax:\n\n```julia\nmd"""\n# Heading 1\n## Heading 2\n### Heading 3\n\n**Bold text**\n*Italic text*\n\n- Bullet point 1\n- Bullet point 2\n\n1. Numbered item 1\n2. Numbered item 2\n\n[Link text](https://example.com)\n"""\n```\n\n### LaTeX Math\n\n```julia\nmd"""\nInline math: $E = mc^2$\n\nDisplay math:\n$$\n\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}\n$$\n"""\n```\n\n### Interpolating Variables\n\nUse `$()` to embed Julia expressions:\n\n```julia\nx = 42\n\nmd"""\nThe value of x is $(x).\n\nComputed value: $(x * 2)\n"""\n```\n\n### Tables in Markdown\n\n```julia\nmd"""\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Value A | Value B | Value C |\n| Value D | Value E | Value F |\n"""\n```\n\n---\n\n## PlutoUI Components\n\nPlutoUI provides interactive widgets using the `@bind` macro.\n\n### Installation\n\n```julia\nbegin\n import Pkg\n Pkg.add("PlutoUI")\n using PlutoUI\nend\n```\n\n### @bind Macro\n\nThe `@bind` macro connects a UI element to a variable:\n\n```julia\n@bind variable_name Widget(options...)\n```\n\n### Available Components\n\n#### 1. Slider\n\nCreates a slider for numeric input.\n\n```julia\n# Basic slider\n@bind x Slider(1:100)\n\n# Slider with default value and display\n@bind temperature Slider(0:0.1:100, default=25, show_value=true)\n\n# Slider with custom range\n@bind alpha Slider(0.0:0.01:1.0, default=0.5)\n```\n\n**Parameters:**\n\n- `range` - Range of values (e.g., `1:10` or `0.0:0.1:1.0`)\n- `default` - Initial value\n- `show_value` - Display current value (true/false)\n\n#### 2. TextField\n\nText input field.\n\n```julia\n# Single-line text field\n@bind name TextField()\n\n# Text field with default value\n@bind description TextField(default="Enter description")\n\n# Multi-line textarea\n@bind notes TextField((50, 10)) # (cols, rows)\n```\n\n**Parameters:**\n\n- `default` - Initial text value\n- `dims=(cols, rows)` - For multi-line textarea\n\n#### 3. NumberField\n\nNumeric input field.\n\n```julia\n# Number field with range\n@bind count NumberField(1:100, default=10)\n\n# Float number field\n@bind value NumberField(0.0:0.1:10.0, default=5.0)\n```\n\n**Parameters:**\n\n- `range` - Valid number range\n- `default` - Initial value\n\n#### 4. CheckBox\n\nBoolean checkbox.\n\n```julia\n# Simple checkbox\n@bind enabled CheckBox()\n\n# Checkbox with default value\n@bind show_details CheckBox(default=true)\n```\n\n**Parameters:**\n\n- `default` - Initial state (true/false)\n\n#### 5. Select\n\nDropdown selection.\n\n```julia\n# Select from array\n@bind color Select(["red", "green", "blue"])\n\n# Select with default\n@bind option Select(["Option A", "Option B", "Option C"], default="Option B")\n\n# Select from pairs (value => label)\n@bind choice Select([1 => "First", 2 => "Second", 3 => "Third"])\n```\n\n**Parameters:**\n\n- `options` - Array of options or pairs\n- `default` - Initially selected value\n\n#### 6. MultiSelect\n\nMultiple selection dropdown.\n\n```julia\n# Multi-select\n@bind colors MultiSelect(["red", "green", "blue", "yellow"])\n\n# Multi-select with defaults\n@bind selected MultiSelect(\n ["A", "B", "C", "D"],\n default=["A", "C"]\n)\n```\n\n#### 7. Button\n\nClickable button.\n\n```julia\n# Simple button\n@bind clicked Button("Click me!")\n\n# Button that increments on each click\n@bind click_count Button("Increment")\n```\n\n**Note**: Button sends the same value each time clicked, triggering reactive cells.\n\n#### 8. Radio\n\nRadio button group (single selection).\n\n```julia\n@bind choice Radio(["Option 1", "Option 2", "Option 3"])\n\n@bind size Radio(["Small", "Medium", "Large"], default="Medium")\n```\n\n#### 9. FilePicker\n\nFile upload widget.\n\n```julia\n@bind uploaded_file FilePicker()\n\n# Access file content\nfile_data = uploaded_file["data"]\nfile_name = uploaded_file["name"]\n```\n\n#### 10. Clock\n\nTimer that ticks at regular intervals.\n\n```julia\n# Tick every second\n@bind tick Clock()\n\n# Tick every 0.5 seconds\n@bind tick Clock(0.5)\n```\n\n#### 11. DateField\n\nDate picker.\n\n```julia\n@bind selected_date DateField()\n\n@bind start_date DateField(default=Dates.today())\n```\n\n---\n\n## Combining Markdown and PlutoUI\n\n### Inline Widgets in Tables\n\nYou can embed PlutoUI widgets directly in markdown tables:\n\n```julia\nmd"""\n| Parameter | Description | Units | Value |\n| --------- | ----------- | ----- | ----- |\n| `T_inf` | Ambient temperature | K | $(@bind T_inf NumberField(0:500, default=300)) |\n| `h` | Heat transfer coefficient | W/(m\xB2\xB7K) | $(@bind h Slider(0.0:0.1:1.0, default=0.7)) |\n| `mass` | Mass | kg | $(@bind m NumberField(0.0:0.1:10.0, default=1.0)) |\n"""\n```\n\n### Using Widget Values in Markdown\n\n```julia\n@bind speed Slider(1:100, default=50, show_value=true)\n\nmd"""\n## Speed Control\n\nCurrent speed: $(speed) km/h\n\nStatus: $(speed > 80 ? "\u{1F534} Fast" : "\u{1F7E2} Normal")\n"""\n```\n\n### Embedded Plots and Computations\n\n```julia\nmd"""\n# Wave Parameters\n\n| Parameter | Value |\n| --------- | ----- |\n| Amplitude | $(@bind amplitude Slider(0.1:0.1:5.0, default=1.0, show_value=true)) |\n| Frequency | $(@bind frequency Slider(0.1:0.1:10.0, default=1.0, show_value=true)) |\n\n## Waveform\n\n$(begin\n x = 0:0.01:2\u03C0\n y = amplitude .* sin.(frequency .* x)\n plot(x, y, label="sin wave", xlabel="x", ylabel="y")\nend)\n"""\n```\n\n### Dynamic Content Generation\n\n```julia\nbegin\n @bind param1 NumberField(0:100, default=50)\n @bind param2 Slider(0.0:0.1:1.0, default=0.5)\n\n result = param1 * param2\n\n md"""\n # Interactive Calculator\n\n | Input | Value |\n | ----- | ----- |\n | Parameter 1 | $(param1) |\n | Parameter 2 | $(param2) |\n | **Result** | **$(result)** |\n\n The computation shows: $(param1) \xD7 $(param2) = $(result)\n """\nend\n```\n\n---\n\n## Best Practices\n\n### 1. Package Management\n\nAlways use a cell at the beginning for package management:\n\n```julia\nbegin\n import Pkg\n Pkg.activate(; temp=true)\n Pkg.add(["Plots", "DataFrames", "PlutoUI"])\n using Plots\n using DataFrames\n using PlutoUI\nend\n```\n\n### 2. Organize with Markdown Headers\n\n```julia\nmd"""\n# Section 1: Data Loading\n\nLoad and prepare the data.\n"""\n\n# ... code cells ...\n\nmd"""\n# Section 2: Analysis\n\nPerform the analysis.\n"""\n\n# ... code cells ...\n```\n\n### 3. Use `begin...end` for Complex Logic\n\n```julia\nbegin\n # Multiple related computations\n data = load_data()\n cleaned = clean_data(data)\n result = analyze(cleaned)\n result\nend\n```\n\n### 4. Display the Last Expression\n\nThe last expression in a cell is automatically displayed:\n\n```julia\nbegin\n x = 10\n y = 20\n x + y # This value is displayed\nend\n```\n\n### 5. Suppress Output with Semicolon\n\n```julia\n# Suppress display\nlarge_data = load_large_dataset();\n\n# Show specific output\nprintln("Data loaded successfully")\n```\n\n### 6. Use @doc for Documentation\n\n```julia\nmd"""\n## Function Documentation\n\n$(@doc my_function)\n"""\n```\n\n### 7. Bind Multiple Related Widgets\n\n```julia\nmd"""\n## Configuration\n\n| Parameter | Value |\n| --------- | ----- |\n| X | $(@bind x Slider(1:10, default=5)) |\n| Y | $(@bind y Slider(1:10, default=5)) |\n\nSum: $(x + y)\nProduct: $(x * y)\n"""\n```\n\n---\n\n## Common Patterns\n\n### Pattern 1: Interactive Parameter Sweep\n\n```julia\n# Define parameters with widgets\n@bind param Slider(1:100, default=50, show_value=true)\n\n# Compute based on parameter\nresult = expensive_computation(param)\n\n# Display results\nplot(result)\n```\n\n### Pattern 2: Conditional Display\n\n```julia\n@bind show_advanced CheckBox(default=false)\n```\n\n```julia\n\nmd"""\nShow advanced options: $(show_advanced ? "\u2705 Enabled" : "\u274C Disabled")\n\n$(if show_advanced\n md"## Advanced Settings\n\n Configure advanced parameters here."\nelse\n md""\nend)\n"""\n```\n\n### Pattern 3: Multi-Step Workflow\n\n```julia\nmd"""\n# Step 1: Select Dataset\n$(@bind dataset Select(["Dataset A", "Dataset B", "Dataset C"]))\n"""\n\n# Load selected dataset\ndata = load_dataset(dataset)\n\nmd"""\n# Step 2: Configure Analysis\n$(@bind threshold Slider(0:0.1:1, default=0.5, show_value=true))\n"""\n\n# Perform analysis\nresults = analyze(data, threshold)\n\nmd"""\n# Step 3: Results\n\n$(plot(results))\n"""\n```\n\n### Pattern 4: Table with Embedded Widgets\n\n```julia\nmd"""\n# Parameter Configuration\n\n| Parameter | Description | Units | Value |\n| --------- | ----------- | ----- | ----- |\n| `temperature` | Operating temperature | \xB0C | $(@bind temp NumberField(0:200, default=25)) |\n| `pressure` | Operating pressure | bar | $(@bind pres NumberField(0:100, default=1)) |\n| `enabled` | Enable feature | - | $(@bind enabled CheckBox(default=true)) |\n"""\n```\n\n```julia\n# Use the bound values\nconfig = (temperature=temp, pressure=pres, enabled=enabled)\n```\n\n### Pattern 5: Real-time Visualization\n\n```julia\n@bind time Clock(0.1) # Update every 0.1 seconds\n\nbegin\n # Generate time-varying data\n t = time\n x = 0:0.1:2\u03C0\n y = sin.(x .+ t)\n\n plot(x, y, label="sin(x + t)", ylims=(-1.5, 1.5))\nend\n```\n\n### Pattern 6: Form-like Interface\n\n```julia\nmd"""\n# User Profile\n\n| Field | Input |\n| ----- | ----- |\n| Name | $(@bind user_name TextField(default="")) |\n| Age | $(@bind user_age NumberField(1:120, default=25)) |\n| Country | $(@bind country Select(["USA", "UK", "Canada", "Other"])) |\n| Subscribe | $(@bind subscribe CheckBox(default=false)) |\n\n$(@bind submit_button Button("Submit"))\n"""\n```\n\n```julia\nbegin\n profile = (\n name = user_name,\n age = user_age,\n country = country,\n subscribe = subscribe\n )\n\n md"""\n ## Submitted Profile\n\n - **Name**: $(profile.name)\n - **Age**: $(profile.age)\n - **Country**: $(profile.country)\n - **Newsletter**: $(profile.subscribe ? "\u2705 Yes" : "\u274C No")\n """\nend\n```\n\n---\n\n## Summary\n\n### Key Takeaways\n\n1. **Reactivity**: Cells automatically re-run based on dependencies\n2. **One Variable Rule**: Each variable can only be defined once across all cells\n3. **`@bind` Macro**: Connects UI widgets to variables\n4. **Markdown Integration**: Use `$()` to embed Julia expressions in markdown\n5. **Tables + Widgets**: Combine markdown tables with PlutoUI for interactive forms\n6. **No Hidden State**: All variables are explicit and traceable\n\n### Quick Reference\n\n**Slider**: `@bind x Slider(1:100, default=50, show_value=true)`\n\n**TextField**: `@bind text TextField(default="")`\n\n**NumberField**: `@bind n NumberField(0:100, default=10)`\n\n**CheckBox**: `@bind checked CheckBox(default=false)`\n\n**Select**: `@bind choice Select(["A", "B", "C"])`\n\n**Button**: `@bind clicked Button("Click")`\n\n**Markdown + Widget**: `$(@bind x Slider(1:10))`\n\n**Markdown + Variable**: `The value is $(x)`\n\n---\n\n## Additional Resources\n\n- Official Pluto.jl: <https://plutojl.org/>\n- PlutoUI Documentation: <https://github.com/JuliaPluto/PlutoUI.jl>\n- Featured Examples: <https://featured.plutojl.org/>\n\n---\n\n**Note for AI Tools**: When creating or modifying Pluto notebooks:\n\n- Always respect the one-variable-per-cell rule\n- Use `begin...end` blocks for multiple statements\n- Ensure cell UUIDs are unique\n- Maintain the cell order section at the end\n- Use `@bind` for all interactive widgets\n- Test reactivity by changing dependent variables\n';
62670
62795
 
62671
62796
  // ../../src/mcp-server-http.ts
62672
62797
  var PlutoMCPHttpServer = class {
@@ -62800,7 +62925,7 @@ var PlutoMCPHttpServer = class {
62800
62925
  );
62801
62926
  server.tool(
62802
62927
  "open_notebook",
62803
- "Open a Pluto notebook file and create a worker session",
62928
+ "Open a Pluto notebook file and create a worker session. The .jl file must already exist on disk \u2014 Pluto will not create a new file from a nonexistent path. Create the file first if needed.",
62804
62929
  {
62805
62930
  path: external_exports.string().describe("Path to the .jl notebook file")
62806
62931
  },
@@ -62870,7 +62995,7 @@ Notebook ID: ${worker2.notebook_id}`
62870
62995
  );
62871
62996
  server.tool(
62872
62997
  "create_cell",
62873
- "Create and execute a new cell in a notebook",
62998
+ "Create and execute a new cell in a notebook. WARNING: This always executes the code. If the code is slow (e.g. package installs), the call may time out but the cell IS still created in Pluto. Use list_cells to check before retrying. For slow operations, prefer edit_cell with run=false then execute_cell separately.",
62874
62999
  {
62875
63000
  path: external_exports.string().describe("Path to the notebook"),
62876
63001
  code: external_exports.string().describe("Julia code for the new cell"),
@@ -62907,7 +63032,7 @@ Notebook ID: ${worker2.notebook_id}`
62907
63032
  );
62908
63033
  server.tool(
62909
63034
  "edit_cell",
62910
- "Update the code of an existing cell",
63035
+ "Update the code of an existing cell. Note: editing the .pluto.jl file on disk has NO effect on the running notebook \u2014 all mutations must go through the MCP API. Use save_notebook to persist changes to disk.",
62911
63036
  {
62912
63037
  path: external_exports.string().describe("Path to the notebook"),
62913
63038
  cell_id: external_exports.string().describe("UUID of the cell to edit"),
@@ -63218,6 +63343,120 @@ Notebook ID: ${worker2.notebook_id}`
63218
63343
  }
63219
63344
  }
63220
63345
  );
63346
+ server.tool(
63347
+ "save_notebook",
63348
+ "Save the running notebook to disk as a .pluto.jl file. Notebooks are NOT auto-saved \u2014 you must call this explicitly to persist changes made via create_cell, edit_cell, or delete_cell.",
63349
+ {
63350
+ path: external_exports.string().describe("Path of the open notebook"),
63351
+ output_path: external_exports.string().describe(
63352
+ "Optional alternative file path to save to (defaults to the notebook's original path)"
63353
+ ).optional()
63354
+ },
63355
+ async ({ path: path4, output_path }) => {
63356
+ if (!this.plutoManager.isConnected()) {
63357
+ throw new Error("Pluto server is not running");
63358
+ }
63359
+ const worker2 = await this.plutoManager.getWorker(path4);
63360
+ if (!worker2) {
63361
+ throw new Error(`Notebook ${path4} is not open`);
63362
+ }
63363
+ const content = this.plutoManager.getNotebookContent(worker2);
63364
+ const savePath = output_path ?? path4;
63365
+ await (0, import_promises.writeFile)(savePath, content, "utf-8");
63366
+ return {
63367
+ content: [
63368
+ {
63369
+ type: "text",
63370
+ text: `Notebook saved to ${savePath} (${content.length} bytes)`
63371
+ }
63372
+ ]
63373
+ };
63374
+ }
63375
+ );
63376
+ server.tool(
63377
+ "delete_cell",
63378
+ "Permanently remove a cell from the notebook by its ID. Use list_cells to find cell IDs.",
63379
+ {
63380
+ path: external_exports.string().describe("Path to the notebook"),
63381
+ cell_id: external_exports.string().describe("UUID of the cell to delete")
63382
+ },
63383
+ async ({ path: path4, cell_id }) => {
63384
+ if (!this.plutoManager.isConnected()) {
63385
+ throw new Error("Pluto server is not running");
63386
+ }
63387
+ const worker2 = await this.plutoManager.getWorker(path4);
63388
+ if (!worker2) {
63389
+ throw new Error(`Notebook ${path4} is not open`);
63390
+ }
63391
+ await this.plutoManager.deleteCell(worker2, cell_id);
63392
+ return {
63393
+ content: [
63394
+ {
63395
+ type: "text",
63396
+ text: `Cell ${cell_id} deleted`
63397
+ }
63398
+ ]
63399
+ };
63400
+ }
63401
+ );
63402
+ server.tool(
63403
+ "list_cells",
63404
+ "List all cells in a notebook with their IDs, code preview, and execution status. Use this to find cell IDs for read_cell, edit_cell, execute_cell, or delete_cell.",
63405
+ {
63406
+ path: external_exports.string().describe("Path to the notebook")
63407
+ },
63408
+ async ({ path: path4 }) => {
63409
+ if (!this.plutoManager.isConnected()) {
63410
+ throw new Error("Pluto server is not running");
63411
+ }
63412
+ const worker2 = await this.plutoManager.getWorker(path4);
63413
+ if (!worker2) {
63414
+ throw new Error(`Notebook ${path4} is not open`);
63415
+ }
63416
+ const snippets = worker2.getSnippets();
63417
+ const cells = snippets.map((snippet, index) => ({
63418
+ cell_id: snippet.cell_id,
63419
+ index,
63420
+ code_preview: snippet.input.code.split("\n")[0].slice(0, 80),
63421
+ errored: snippet.result.errored,
63422
+ running: snippet.result.running,
63423
+ queued: snippet.result.queued
63424
+ }));
63425
+ return {
63426
+ content: [
63427
+ {
63428
+ type: "text",
63429
+ text: JSON.stringify({ count: cells.length, cells }, null, 2)
63430
+ }
63431
+ ]
63432
+ };
63433
+ }
63434
+ );
63435
+ server.tool(
63436
+ "get_notebook_url",
63437
+ "Get the browser URL to open the notebook in Pluto's web interface",
63438
+ {
63439
+ path: external_exports.string().describe("Path to the notebook")
63440
+ },
63441
+ async ({ path: path4 }) => {
63442
+ if (!this.plutoManager.isConnected()) {
63443
+ throw new Error("Pluto server is not running");
63444
+ }
63445
+ const worker2 = await this.plutoManager.getWorker(path4);
63446
+ if (!worker2) {
63447
+ throw new Error(`Notebook ${path4} is not open`);
63448
+ }
63449
+ const url2 = `${this.plutoManager.getServerUrl()}/edit?id=${worker2.notebook_id}`;
63450
+ return {
63451
+ content: [
63452
+ {
63453
+ type: "text",
63454
+ text: url2
63455
+ }
63456
+ ]
63457
+ };
63458
+ }
63459
+ );
63221
63460
  }
63222
63461
  setupRoutes() {
63223
63462
  this.app.get("/mcp", async (_req, res) => {
@@ -63623,10 +63862,10 @@ var NodeServerManager = class {
63623
63862
  };
63624
63863
 
63625
63864
  // ../../src/cli/nodeFileReader.ts
63626
- var import_promises = require("fs/promises");
63865
+ var import_promises2 = require("fs/promises");
63627
63866
  var NodeFileReader = class {
63628
63867
  async readFile(path4) {
63629
- return await (0, import_promises.readFile)(path4, "utf-8");
63868
+ return await (0, import_promises2.readFile)(path4, "utf-8");
63630
63869
  }
63631
63870
  };
63632
63871
 
@@ -63649,45 +63888,52 @@ var consoleLogger = {
63649
63888
  // ../../src/cli/run.ts
63650
63889
  async function run(config) {
63651
63890
  console.log("@plutojl/mcp \u2014 Standalone MCP server for Pluto.jl\n");
63652
- let serverManager;
63653
- let plutoManager;
63654
- if (config.plutoUrl) {
63655
- console.log(
63656
- `[cli] Connecting to existing Pluto server at ${config.plutoUrl}`
63657
- );
63658
- serverManager = new NodeServerManager(
63659
- config.plutoPort,
63660
- config.juliaVersion,
63661
- config.workDir
63662
- );
63663
- plutoManager = new PlutoManager(
63664
- config.plutoPort,
63665
- consoleLogger,
63666
- serverManager,
63667
- new NodeFileReader(),
63668
- config.plutoUrl
63669
- );
63670
- } else {
63671
- serverManager = new NodeServerManager(
63672
- config.plutoPort,
63673
- config.juliaVersion,
63674
- config.workDir
63675
- );
63676
- plutoManager = new PlutoManager(
63677
- config.plutoPort,
63678
- consoleLogger,
63679
- serverManager,
63680
- new NodeFileReader()
63681
- );
63682
- }
63891
+ const serverManager = new NodeServerManager(
63892
+ config.plutoPort,
63893
+ config.juliaVersion,
63894
+ config.workDir
63895
+ );
63896
+ const plutoManager = new PlutoManager(
63897
+ config.plutoPort,
63898
+ consoleLogger,
63899
+ serverManager,
63900
+ new NodeFileReader(),
63901
+ config.plutoUrl
63902
+ );
63683
63903
  const mcpServer = new PlutoMCPHttpServer(plutoManager, config.mcpPort, true);
63684
63904
  await mcpServer.start();
63685
63905
  console.log(
63686
- `
63687
- [cli] MCP server listening at http://localhost:${config.mcpPort}/mcp`
63906
+ `[cli] MCP server listening at http://localhost:${config.mcpPort}/mcp`
63688
63907
  );
63689
63908
  console.log(`[cli] Health check: http://localhost:${config.mcpPort}/health`);
63690
- console.log(`[cli] Press Ctrl+C to stop
63909
+ if (!config.noPluto) {
63910
+ if (config.plutoUrl) {
63911
+ console.log(
63912
+ `[cli] Connecting to existing Pluto server at ${config.plutoUrl}...`
63913
+ );
63914
+ } else {
63915
+ console.log(`[cli] Starting Pluto server on port ${config.plutoPort}...`);
63916
+ }
63917
+ try {
63918
+ await plutoManager.start();
63919
+ console.log(`[cli] Pluto server is ready.`);
63920
+ } catch (err) {
63921
+ console.error(
63922
+ `[cli] Failed to start Pluto: ${err instanceof Error ? err.message : String(err)}`
63923
+ );
63924
+ console.error(
63925
+ `[cli] MCP server is still running \u2014 you can start Pluto later via:`
63926
+ );
63927
+ console.error(`[cli] npx @plutojl/mcp call start_pluto_server`);
63928
+ }
63929
+ } else {
63930
+ console.log(`[cli] Pluto auto-start skipped (--no-pluto).`);
63931
+ console.log(
63932
+ `[cli] Start it later: npx @plutojl/mcp call start_pluto_server`
63933
+ );
63934
+ }
63935
+ console.log(`
63936
+ [cli] Press Ctrl+C to stop
63691
63937
  `);
63692
63938
  console.log(`Tip: Run 'npx @plutojl/mcp install' to configure Claude Code
63693
63939
  `);
@@ -63944,6 +64190,44 @@ async function callTool(port, toolName, argsJson, raw2) {
63944
64190
  }
63945
64191
  }
63946
64192
 
64193
+ // ../../src/cli/preflight.ts
64194
+ async function preflight(port, opts = {}) {
64195
+ const mcpUrl = `http://localhost:${port}`;
64196
+ let health;
64197
+ try {
64198
+ const res = await fetch(`${mcpUrl}/health`, {
64199
+ signal: AbortSignal.timeout(3e3)
64200
+ });
64201
+ health = await res.json();
64202
+ } catch {
64203
+ console.error(`Error: Cannot reach MCP server at ${mcpUrl}`);
64204
+ console.error(``);
64205
+ console.error(`The MCP server is not running. Start it first:`);
64206
+ console.error(``);
64207
+ console.error(` npx @plutojl/mcp run`);
64208
+ console.error(``);
64209
+ console.error(
64210
+ `Or, if you are using a different port, pass --mcp-port <port>.`
64211
+ );
64212
+ process.exit(1);
64213
+ }
64214
+ if (opts.requirePluto && !health.plutoServerRunning) {
64215
+ console.error(`Error: MCP server is running but Pluto is not connected.`);
64216
+ console.error(``);
64217
+ console.error(`Start Pluto via the MCP server:`);
64218
+ console.error(``);
64219
+ console.error(` npx @plutojl/mcp call start_pluto_server`);
64220
+ console.error(``);
64221
+ console.error(`Or connect to an existing Pluto server:`);
64222
+ console.error(``);
64223
+ console.error(
64224
+ ` npx @plutojl/mcp call connect_to_pluto_server '{"port": 1234}'`
64225
+ );
64226
+ process.exit(1);
64227
+ }
64228
+ return { mcpRunning: true, plutoRunning: health.plutoServerRunning };
64229
+ }
64230
+
63947
64231
  // ../../src/cli/index.ts
63948
64232
  async function main() {
63949
64233
  const args = parseArgs(process.argv.slice(2));
@@ -63960,6 +64244,7 @@ async function main() {
63960
64244
  break;
63961
64245
  }
63962
64246
  case "tools": {
64247
+ await preflight(mcpPort);
63963
64248
  await listTools(mcpPort);
63964
64249
  break;
63965
64250
  }
@@ -63968,6 +64253,7 @@ async function main() {
63968
64253
  console.error("Usage: npx @plutojl/mcp call <tool_name> [json_args]");
63969
64254
  process.exit(1);
63970
64255
  }
64256
+ await preflight(mcpPort);
63971
64257
  await callTool(
63972
64258
  mcpPort,
63973
64259
  args.toolName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plutojl/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Standalone MCP server for Pluto.jl notebooks — run with npx @plutojl/mcp",
5
5
  "license": "MIT",
6
6
  "repository": {