Enabling MCP
Introduction
If you have an existing FrontPanel app and want to make it MCP-capable, the path is:
- Confirm the app’s structure. MCP integrates most easily with apps that already follow the Driver / WorkQueue / shared-handler pattern. If UI controls currently use inline handlers, those handlers should be lifted to the root first. This is the largest part of the work and improves the UI path independently of MCP.
- Choose the tool surface. List the reads, writes, and one-shot actions an AI should be able to perform. This list is the contract that AI clients will see.
- Add three files and modify two. Declare opt-in in
frontpanel-app.json, list tools inmcp.json, implement bindings inMCPBindings.ts, and register them at app startup. The full step-by-step is in the developer guide. - Verify with a real client. Connect Claude Desktop or Claude Code, list the tools, invoke a write tool from the AI, and confirm the corresponding UI control updates. A round-trip from AI to Platform to handler to gateware and back, with the UI in sync, confirms the integration.
New apps can start from a Platform-app scaffold that ships with the MCP wiring in place. Existing apps follow the integration guide section by section. Two reference apps illustrate the pattern at different scales — see Reference apps below.
Reference Apps
Two MCP-capable apps ship as worked examples. Both are fully functional FrontPanel Platform apps; both have their MCP integration wired through the same patterns described in the developer guide. Use them as templates and as reference material when designing your own tool surface.
Counters
Counters drives two simple peripherals on the FPGA: an autonomous counter with an enable line, and a stepwise counter with an optional auto-count mode. It is the smallest useful MCP-capable app and is the recommended starting point for learning the pattern. The MCP surface is six tools across the two counters.
| Tool | Kind | Description |
|---|---|---|
counter1_reset | action | Pulse the reset line for counter 1, returning it to zero. |
counter1_set_enabled | write | Enable or disable counting on counter 1. When disabled the counter holds its current value. |
counter1_read | read | Read the current value of counter 1. |
counter2_step | action | Step counter 2 by one in the chosen direction ("up" or "down"). |
counter2_set_auto_count | write | Toggle counter 2’s autonomous-count mode. |
counter2_read | read | Read the current value of counter 2. |
Example Session — Claude Desktop
A short conversational interaction with Counters. The user’s requests are in italics; the AI’s tool calls and the values returned are shown beneath each one.
User: <em>What are the counters reading?</em>
→ counter1_read() → 4218
→ counter2_read() → 17
AI: Counter 1 is at 4218 and counter 2 is at 17.
User: <em>Stop counter 1 and reset it, then step counter 2 down by five.</em>
→ counter1_set_enabled({ enabled: false })
→ counter1_reset()
→ counter2_step({ direction: "down" }) × 5
→ counter1_read() → 0
→ counter2_read() → 12
AI: Counter 1 is held at 0; counter 2 is now at 12.Code language: plaintext (plaintext)Each tool call in the trace also produces a UI-side update — the user watches the on-screen counter values change in real time as the AI works through the request.
FFTScope — Multi-Instrument Coordination
FFTScope is a software-defined signal-chain demo: a waveform generator drives an output, a two-channel digitizer captures the result, and a spectrum / spectrogram view displays the FFT. It is the larger of the two reference apps and is useful when designing an MCP surface that spans several cooperating instruments. The MCP surface is 21 tools across three subsystems.
Waveform Generator (7 Tools)
Eight independent frequency-vector slots are summed by an IFFT engine. Each slot has an FFT bin number, a dBFS amplitude, and an enable flag.
waveform_initialize— bring the IFFT engine up to ResetComplete and replay saved vectors.waveform_reset— clear all eight slots and silence the output.waveform_set_vector— configure one slot (vector_id,bin,amplitude_dbfs,enabled).waveform_set_autoscale— enable IFFT-clip protection (sum of amplitudes clamped to ≤ 1.0).waveform_get_vectors— read the current state of all eight slots.waveform_get_sample_rate— read DAC sample rate, IFFT length, and per-bin frequency spacing.waveform_get_amplitude_scale— read the active autoscale divisor.
Digitizer (7 Tools)
Two-channel ADC capture, with both a continuous-refresh loop and a single-shot mode.
digitizer_reset— reset the digitizer state machine.digitizer_set_channel_enabled— show or hide a channel’s trace.digitizer_set_continuous— start or stop the periodic capture loop (≈ 35 ms refresh).digitizer_capture_once— capture one 1024-sample frame on both channels.digitizer_read_samples— read the most recent capture as raw signed 16-bit ADC values.digitizer_get_sample_rate— read the ADC sample rate in Hz.digitizer_get_channel_enabled— read whether a channel is currently displayed.
Spectrum / Spectrogram (7 Tools)
FFT-domain display of the most recent capture, with a switchable spectrogram view.
spectrum_set_mode/spectrum_get_mode— switch between live spectrum and rolling spectrogram.spectrum_set_noise_floor/spectrum_get_noise_floor— spectrogram floor in dBFS.spectrum_set_gamma/spectrum_get_gamma— spectrogram contrast curve.spectrum_read_latest— read the most recent FFT magnitudes (512-element dBFS array per channel).
Example Session — Claude Code with Scripting
FFTScope is the canonical example of where the script tool earns its keep: configure a tone, capture, locate the peak, report. The AI writes a short script and submits it as one MCP call.
User: <em>Drive a 5 kHz tone at -6 dBFS, capture, and tell me which FFT bin holds
the peak and what its level is.</em>
The AI writes and runs:
const { sample_rate, fft_length } = await mcp.tools.waveform_get_sample_rate();
const targetHz = 5000;
const bin = Math.round(targetHz * fft_length / sample_rate);
await mcp.tools.waveform_reset();
await mcp.tools.waveform_set_vector({
vector_id: 0, bin, amplitude_dbfs: -6, enabled: true,
});
await mcp.tools.digitizer_capture_once();
const { channel1 } = await mcp.tools.spectrum_read_latest();
let peakBin = 0, peakDb = -Infinity;
for (let i = 1; i < channel1.length; i++) {
if (channel1[i] > peakDb) { peakDb = channel1[i]; peakBin = i; }
}
const peakHz = peakBin * sample_rate / fft_length;
return { peakBin, peakHz, peakDb };
AI: The peak is at bin 51 (4980 Hz) at -6.2 dBFS, which matches the
requested 5 kHz tone within one bin of resolution.Code language: JavaScript (javascript)The script makes seven tool calls but uses only one round-trip on the MCP wire — a meaningful difference for sweeps or any iterative measurement. The script body itself is a useful artifact: the engineer can refine it, parameterize the frequency, and check it in as a regression test.
Next Steps
Start with Counters when learning the integration pattern: it is small enough to read end-to-end in one sitting, and the six tools cover the three tool kinds (read, write, action) without distraction. Move to FFTScope when designing for an app that has multiple cooperating instruments, several state machines, or a tool surface large enough that naming conventions and grouping matter.
MCP Manifest
Two small files describe your app’s MCP surface. Together they tell FrontPanel Platform what to expose and how to validate calls coming in.
App Manifest – Opt-in and Permissions
The app manifest your app already ships gets a new permissions.mcp block. This is the gate: without it, MCP for your app is off, full stop.
{
"appGuid": "...",
"name": "Counters",
"permissions": {
"mcp": {
"bindings": true, // expose this app's tools to AI clients
"script": true // allow run_javascript for batched calls
}
}
}Code language: JSON / JSON with Comments (json)Two independent switches:
bindings— expose the tools you declare inmcp.json. This is the typical “AI can drive my app” switch.script— additionally expose arun_javascripttool that lets the AI batch several of your tools into one local execution. Big efficiency win for multi-step workflows; off by default if you’d rather keep the surface narrow.
The Tool List (mcp.json)
Sits next to your app source. It’s the contract you ship to AI clients. Each entry names a tool, describes it in one sentence (the AI reads this to decide when to call it), and specifies a JSON Schema for the inputs.
{
"tools": [
{
"name": "counter1_set_enabled",
"description": "Enable or disable counter 1.",
"inputSchema": {
"type": "object",
"properties": { "enabled": { "type": "boolean" } },
"required": ["enabled"],
"additionalProperties": false
}
},
{
"name": "counter1_read",
"description": "Read the current value of counter 1.",
"inputSchema": { "type": "object", "additionalProperties": false }
}
]
}Code language: JSON / JSON with Comments (json)At app launch, your code registers a handler function for each declared name. The Platform enforces the relationship in both directions: a tool declared but never registered is invisible, and a name your code tries to register that isn’t in mcp.json is rejected. The manifest is the upper bound; the registered set is the truth.
Those two files describe the surface. The behavior of each tool — what it does to your gateware — lives in your normal app code.
JavaScript Scripting
When an app declares permissions.mcp.script, FrontPanel Platform synthesizes one extra tool — run_javascript — alongside the tools the app declared in mcp.json. The AI calls it like any other tool, passing a string of JavaScript source. The Platform runs that script in a sandboxed renderer dedicated to the app, and the script can call the app’s other tools as ordinary await mcp.tools.<name>(args) JavaScript expressions.
The script runs locally — every inner tool call is an in-process IPC, not an MCP wire round-trip — but the resulting calls travel the same path as any other tool call. UI clicks, direct AI tool calls, and inner script calls all converge on the same shared handler and post to the same per-device WorkQueue.

Script Sandbox
The sandbox exposes a single object — mcp.tools — passed in as a parameter to the AI’s code body. It is a plain JavaScript object whose keys are the names of every tool the app registered (e.g. counter1_read, digitizer_capture_once, and so on), and whose values are async functions. Each function takes the tool’s argument object and returns a Promise that resolves with the tool’s normal return value or rejects with the error the tool produced. The script can call them in any order, branch on results, accumulate data, and return any JSON-serializable value — that return value is what the AI receives as the run_javascript tool result.
The execution environment is deliberately minimal:
- No DOM, no
fetch, no Node APIs. - No filesystem, no network, no access to other apps or to the launcher.
- No reach into the app renderer’s React state — the script can only observe app state through tools the app chose to expose.
mcp.toolsis passed in as a parameter rather than living onglobalThis, so prototype tampering inside the main world cannot redirect it.
Standard JavaScript built-ins (Math, Array, Promise, JSON, etc.) are available, so any computation the AI wants to do on tool results — averages, peak finding, FFT post-processing, conditional control flow — happens locally in the sandbox without further round-trips.
Convergent Paths at the Work Queue
The WorkQueue is a per-device FIFO that serializes everything the app sends to the FrontPanel SDK. Without it, a UI click, a direct AI tool call, and an inner call from a running script could interleave on the wire in arbitrary order — corrupting device state in ways that look like sporadic firmware bugs.
Because every invocation path eventually posts to the same queue, the device sees one well-ordered stream of operations regardless of who issued them. This is also why integration guidance asks app developers to lift handlers to the root and have UI controls call them: the UI button and the MCP tool are two callers of one function, not parallel implementations of the same behavior.
Trust & Control
Access is controlled at three layers, all visible to the user.
- Per-app opt-in. An app without
permissions.mcpin its manifest is invisible to AI clients. There is no default-on; apps that do not want this surface are not part of it. - Per-app override in the Platform. An app that requests MCP can be denied in Platform’s MCP Settings. The override is revoke-only: the user can narrow what an app receives, never expand it past what the manifest declared.
- Live activity window. Every tool call appears in real time, with the app, tool, arguments, outcome, and duration. Recent history is retained in a rolling audit log.
Tool calls pass through schema validation, a per-call timeout, and (for scripted calls) a sandboxed renderer with no DOM, network, or filesystem access. The AI’s reach is bounded to the tools you declared: it cannot browse the disk, communicate with other apps, or extend its surface mid-session.
What the Platform does not enforce. The Platform validates that only declared tools with valid arguments are called, but it does not constrain what those tools do. If erase_flash is exposed as a tool, the AI can call it. Choose the exposed surface with the same care you would apply to a public API.