Résultats pour
Do we know if MATLAB is being used on the Artemis II (moon mission) spacecraft itself? Like is the crew running MATLAB programs? I imagine it was probably at least used in development of some of the components of the spacecraft, rockets, or launch building. Or is it used for any of the image analysis of the images collected by the spacecraft?
MATLAB interprets the first block of uninterupted comments in a function file as documentation. Consider a simple example.
% myfunc This is my function
%
% See also sin
function z = myfunc(x, y)
z = x + y;
end
Those comments are printed in the command window with "help myfunc" and displayed in a separate window with "doc myfunc". A lot of useful things happen behind the scenes as well.
- Hyperlinks are automatically added for valid file names after "See also".
- When dealing with classes, the doc command automatically appends the comment block with a lists of properties and methods.
All this is very handy and as been around for quite some time. However, the doc browser isn't great (forward/back feature was removed several versons ago), the text formatting isn't great, and there is no way to display math.
Although pretty text/math can be displayed in a live document, the traditional *.mlx file format does not always play nice with Git and I have avoided them. However, live documents can now (since 2025a?) be saved in a pure text format, so I began to wonder if all functions should be written in this style. Turns out that all you have to do is append these lines:
%[appendix]{"version":"1.0"}
%---
to the end of any function file to make it a live function. Doing so changes how MATLAB manages that first comment block. The help command seems to be unaffacted, although [text] may appear at the start of each comment line (depending on if the file was create as a live function or subsequently converted). The doc command behaves very different: instead of bringing up the traditional window for custom documentation, the comment block looks like it gets published to HTML and looks more similar to standard MATLAB help. This is a win in some ways, but the "See also" capabilitity is lost.
Curiously, the same text can be appended to the end of a class definition file with some affect. It does not change how the file shows up in the editor, but as in live functions, comments are published when using the doc command. So we are partway to something like a "live class", but not quite.
Should one stick with traditional *.m files or make everything live? Neither does a great job for functions/classes in a namespace--references must explicitly know absolute location in traditional functions, and there is no "See also" concept in a live function. Do we need a command, like cdoc (custom documentation), that pulls out the comment block, publishing formatted text to HTML while simultaneously resolving "See also" references as hyperlinks? If so, it would be great if there were other special commands like "See examples" that would automatically copy and then open an example script for the end user.
Hi all,
I'm a UX researcher here at MathWorks working on the MathWorks Central Community. We're testing a new feature to make it easier to ask a question, and we'd love to hear from community members like you.
Sessions will be next week. They are remote, up to 2 hours (often shorter), and participants receive a $100 stipend. If you're interested, you can click here to schedule.
Thanks in advance! Your feedback directly shapes what gets built.
--David, MathWorks UX Research
Absolutely!
64%
Probably
9%
Sometimes yes, sometimes no
5%
Unlikely
18%
Never!
5%
22 votes
Matlab seems to follow a rule that iterative reduction operators give appropriate non-empty values to empty inputs. Examples include,
sum([])
prod([])
all([])
any([])
Is it an oversight not to do something similar for min and max?
max([])
For non-empty A and B,
max([A,B])= max(max(A), max(B))
The extension to B=[] should therefore satisfy,
max(A)=max(max(A),max([]))
for any A, which will only be true if we define max([])=-inf.
The 550,000th question has been asked on Answers.
Have you ever wondered what it takes to send live audio from one computer to another? While we use apps like Discord and Zoom every day, the core technology behind real-time voice communication is a fascinating blend of audio processing and networking. Building a simple walkie-talkie is a perfect project for demystifying these concepts, and you can do it all within the powerful environment of MATLAB.
This article will guide you through creating a functional, real-time, push-to-talk walkie-talkie. We won't be building a replacement for a commercial radio, but we will create a powerful educational tool that demonstrates the fundamentals of digital signal processing and network communication.
The Purpose: Why Build This?
The goal isn't just to talk to a colleague across the office; it's to learn by doing. By building this project, you will:
Understand Audio I/O: Learn how MATLAB interacts with your computer’s microphone and speakers.
Grasp Network Communication: See how to send data packets over a local network using the UDP protocol.
Solve Real-Time Challenges: Confront and solve issues like latency, choppy audio, and continuous data streaming.
The Core Components
Our walkie-talkie will consist of two main scripts:
Sender.m: This script will run on the transmitting computer. It listens to the microphone when a key is pressed, sending the audio data in small chunks over the network.
Receiver.m: This script runs on the receiving computer. It continuously listens for incoming data packets and plays them through the speakers as they arrive.
Step 1: Getting Audio In and Out
Before we touch networking, let's make sure we can capture and play audio. MATLAB's built-in audiorecorder and audioplayer objects make this simple.
Problem Encountered: How do you even access the microphone?
Solution: The audiorecorder object gives us straightforward control.
code
% --- Test Audio Capture and Playback ---
Fs = 8000; % Sample rate in Hz
nBits = 16; % Number of bits per sample
nChannels = 1; % Mono audio
% Create a recorder object
recObj = audiorecorder(Fs, nBits, nChannels);
disp('Start speaking for 3 seconds.');
recordblocking(recObj, 3); % Record for 3 seconds
disp('End of Recording.');
% Get the audio data
audioData = getaudiodata(recObj);
% Play it back
playObj = audioplayer(audioData, Fs);
play(playObj);
Running this script confirms that your microphone and speakers are correctly configured and accessible by MATLAB.
Step 2: Sending Voice Over the Network
Now, we need to send the audioData to another computer. For real-time applications like this, the UDP (User Datagram Protocol) is the ideal choice. It’s a "fire-and-forget" protocol that prioritizes speed over perfect reliability. Losing a tiny packet of audio is better than waiting for it to be re-sent, which would cause noticeable delays (latency).
Problem Encountered: How do you send data continuously without overwhelming the network or the receiver?
Solution: We'll send the audio in small, manageable chunks inside a loop. We need to create a UDP Port object to handle the communication.
Here's the basic structure for the Sender.m script:
code
% --- Sender.m ---
% Define network parameters
remoteIP = '192.168.1.101'; % <--- CHANGE THIS to the receiver's IP
remotePort = 3000;
localPort = 3001;
% Create UDP Port object
udpSender = udpport("LocalPort", localPort, "EnablePortSharing", true);
% Configure audio recorder
Fs = 8000;
nBits = 16;
nChannels = 1;
recObj = audiorecorder(Fs, nBits, nChannels);
disp('Press any key to start transmitting. Press Ctrl+C to stop.');
pause; % Wait for user to press a key
% Start the Push-to-Talk loop
disp('Transmitting... (Hold Ctrl+C to exit)');
while true
recordblocking(recObj, 0.1); % Record a 0.1-second chunk
audioChunk = getaudiodata(recObj);
% Send the audio chunk over UDP
write(udpSender, audioChunk, "double", remoteIP, remotePort);
end
And here is the corresponding Receiver.m script:
code
Matlab
% --- Receiver.m ---
% Define network parameters
localPort = 3000;
% Create UDP Port object
udpReceiver = udpport("LocalPort", localPort, "EnablePortSharing", true, "Timeout", 30);
% Configure audio player
Fs = 8000;
playerObj = audioplayer(zeros(Fs*0.1, 1), Fs); % Pre-buffer
disp('Listening for incoming audio...');
% Start the listening loop
while true
% Wait for and receive data
[audioChunk, ~, ~] = read(udpReceiver, Fs*0.1, "double");
if ~isempty(audioChunk)
% Play the received audio chunk
play(playerObj, audioChunk);
else
disp('No data received. Still listening...');
end
end
Step 3: Solving Real-World Hurdles
Running the code above might work, but you'll quickly notice some issues.
Problem 1: Choppy Audio and High Latency
The audio might sound robotic or delayed. This is because of the buffer size and the processing time. Sending tiny chunks frequently can cause overhead, while sending large chunks causes delay.
Solution: The key is to find a balance.
Tune the Chunk Size: The 0.1 second chunk size in the sender (recordblocking(recObj, 0.1)) is a good starting point. Experiment with values between 0.05 and 0.2. Smaller values reduce latency but increase network traffic.
Use a Buffered Player: Instead of creating a new audioplayer for every chunk, we create one at the start and feed it new data. Our receiver code already does this, which is more efficient.
Problem 2: No Real "Push-to-Talk"
Our sender script starts transmitting and doesn't stop. A real walkie-talkie only transmits when a button is held down.
Solution: Simulating this in a script requires a more advanced technique, ideally using a MATLAB App Designer GUI. However, we can create a simple command-window version using a figure's KeyPressFcn.
Here is an improved concept for the Sender that simulates radio push-to-talk, e.g. https://www.retevis.com/blog/ptt-push-to-talk-walkie-talkies-guide
% --- Advanced_Sender.m ---
function PushToTalkSender()
% -- Configuration --
remoteIP = '192.168.1.101'; % <--- CHANGE THIS
remotePort = 3000;
localPort = 3001;
Fs = 8000;
% -- Setup --
udpSender = udpport("LocalPort", localPort);
recObj = audiorecorder(Fs, 16, 1);
% -- GUI for key press detection --
fig = uifigure('Name', 'Push-to-Talk (Hold ''t'')', 'Position', [100 100 300 100]);
fig.KeyPressFcn = @KeyPress;
fig.KeyReleaseFcn = @KeyRelease;
isTransmitting = false; % Flag to control transmission
disp('Focus on the figure window. Hold the ''t'' key to transmit.');
% --- Main Loop ---
while ishandle(fig)
if isTransmitting
% Non-blocking record and send would be ideal,
% but for simplicity we use short blocking chunks.
recordblocking(recObj, 0.1);
audioChunk = getaudiodata(recObj);
write(udpSender, audioChunk, "double", remoteIP, remotePort);
disp('Transmitting...');
else
pause(0.1); % Don't burn CPU when idle
end
drawnow; % Update figure window
end
% --- Callback Functions ---
function KeyPress(~, event)
if strcmp(event.Key, 't')
isTransmitting = true;
end
end
function KeyRelease(~, event)
if strcmp(event.Key, 't')
isTransmitting = false;
disp('Transmission stopped.');
end
end
end
Conclusion and Next Steps
You've now built the foundation of a real-time voice communication tool in MATLAB! You've learned how to capture audio, send it over a network using UDP, and handle some of the fundamental challenges of real-time streaming.
This project is the perfect starting point for more advanced explorations:
Build a Full GUI: Use App Designer to create a user-friendly interface with a proper push-to-talk radio button.
Implement Noise Reduction: Apply a filter (e.g., a simple low-pass or a more advanced spectral subtraction algorithm) to the audioChunk before sending it.
Add Channels: Modify the code to use different UDP ports, allowing users to select a "channel" to talk on.
In a previous discussion,
we looked at a variety of infallible tests for primality, but all of them were too slow to be viable for large numbers. In fact, all of the methods discussed there will fail miserably for even moderately large numbers, those with just a few dozen decimal digits. That does not even begin to push into the realm of large numbers. In turn, that forces us into a different realm of tests - tests which are usually and even almost always correct, but can sometimes incorrectly predict primality.
In this discussion, I will be trying to convince you that the Fermat test for primality can be a quite good test when the number is sufficiently large. Except of course, when it is really bad. Even so, the Fermat test for primality is both a useful tool as well as a necessary underpinning for several other better tests.
The Fermat test for primality relies on Fermat's little theorem, a perhaps under-appreciated tool. Even the name implies it is of little interest. I'm not taking about the famous last theorem, only proven in recent years, but his little theorem.
If you want to look over some nice ways to prove the little theorem, take a read in this link:
I will readily admit that long ago, when I learned about the little theorem in a nearly forgotten class, I thought it was interesting, but why would I care? Not until I learned more mathematics and saw Fermat’s little theorem appearing in different places did I begin to appreciate it. Fermat tells us that, IF P is a prime, AND w is co-prime with P (so the two are relatively prime, sharing no common factors except 1), then it must be true that
mod(w^(P-1),P) == 1
Try it out. Does it work? Be careful though as too large of an exponent will cause problems in double precision, and that is not difficult to do. As a test case that will not overwhelm doubles, note that 13 is prime, and 3 shares no common factors with 13, so we satisfy the requirements for Fermat's little theorem.
mod(3^12,13)
We can even verify that any co-prime of 13 will yield the same result.
mod((1:12).^12,13)
Indeed it worked, suggesting what we knew all along, that 13 is prime. The little Fermat test for primality of the number P uses a converse form of Fermat's little theorem, thus given a co-prime number w known as the witness, is that if
mod(w^(P-1),P)==1
then we have evidence that P is indeed prime. This is not conclusive evidence, but still it is evidence. It is not conclusive because the converse of a true statement is not always true.
The analogy I like here is if we lived in a universe where all crows are black. (I'll ask you to pretend this is true. In fact, some crows have a mutation, making them essentially albino crows. For the purposes of this thought experiment, pretend this cannot happen.) Now, suppose I show you a picture of a bird which happens to be black. Do you know the bird to be a crow? Of course not, as the bird may be a raven, or a redwing blackbird (until you see the splash of red on the wing), a common grackle, a European starling in summer plumage, a condor, etc. But having seen black plumage, it is now more likely the bird is indeed a crow. I would call this a moderately weak evidentiary test for crow-ness.
Little Fermat may seem to be of little value when testing for primality for two reasons. First, computing the remainder would seem to be highly CPU intensive for large P. In the example above, I had only to compute 3^12=531441, which is not that large. But for numbers with many thousands or millions of digits, directly raising even a number as small as 2 to that power will overwhelm any computer. Secondly, if we do that computation, little Fermat does not conclusively prove P to be prime.
Our savior in one respect is the powermod tool. And that helps greatly, since we can compute the remainder in a reasonable time. A powermod call is quite fast even for huge powers. (I won't get into how powermod works here, since that alone is probably worth a discussion. I could though, if I see some interest because there are some very pretty variations of the powermod algorithm. I hope to show you one of them when I discuss the Fibonacci test for primality in a future post.) Trying the little Fermat test using powermod on a number with 1207 decimal digits, I’ll first do a time check.
P = 4000*sym(2)^3999 - 1;
timeit(@() powermod(2,P-1,P))
As you can see, powermod really is pretty fast. Compared to an isprime test on that number it would show a significant difference.
I have said before that little Fermat is not a conclusive test. In fact, a good trick is to perform a second little Fermat test, using a different witness. If the second test also indicates primality, then we have additional evidence that P is in fact prime.
w = [2 3]; % Two parallel witnesses
powermod(w,P-1,P)
This value for P is indeed prime, and little Fermat suggests it is, doubly suggestive in that test since I actually performed two parallel tests. Here however, we need to understand when it will fail, and how often it will fail.
If we perform a little Fermat test for primality, we will never see false negatives, that is, if any test with any witness ever indicates a number is composite, then it is certainly composite. (The contrapositive of a true statement is always true.) The alternate class of failure is the false positive, where little Fermat indicates a number is prime when it was actually composite.
If P is composite, and w co-prime with P, we call P a Fermat pseudo-prime for the witness w if we see a remainder of 1 when P was in fact composite. When that happens, the witness (w) is called a Fermat liar for the primality of P. (A list of some Fermat pseudo-primes where 2 is a Fermat liar can be found in sequence A001567 of the OEIS.)
In the case of 4000*2^3999-1, I claimed the number to be in fact prime, and it was identified so (as PROBABLY prime by little Fermat. Next, consider another number from that same family. I’ll perform three parallel tests on it, with witnesses 2, 3, and 5. This will suggest the value of doing parallel tests on a number to reduce the failure rate from little Fermat.
P2 = 1024*sym(2)^1023 - 1;
w = [2; 3; 5];
gcd(w,P2)
F2 = powermod(w,P2-1,P2)
logical(F2 == 1)
As you can see, P2 is co-prime with all of 2, 3 and 5, but 2 is a Fermat liar, whilst 3 and 5 are Fermat truth tellers, identifying P2 as certainly composite. So the little Fermat test can definitely fail for SOME witnesses, since we see a disagreement. However, an interesting fact about P2 above is it is also a Mersenne number with prime exponent, thus it can be written as 2^1033-1, where 1033 is prime. I can go into more detail about this case later, but we can show that 2 is always a Fermat liar for composite Mersenne numbers when the exponent is itself prime. I’ll try to leave more detail on this matter in a future discussion, or perhaps a comment.
Next, consider the composite integer 51=3*17. As the product of two primes, it is clearly not itself prime.
P51 = 51;
w0 = 2:P51-2;
w = w0(gcd(w0,P51) == 1)
Note that I did not include 1 or 50 in that set, since 1 raised to any power is 1, and 50 is congruent to -1, mod 51. -1 raised to any even power is also always 1, and 51-1 is an even number. And so when we are working modulo 51, both 1 and 50 are not useful witnesses in terms of the little Fermat test.
R = powermod(w,P-1,P)
w(R == 1)
This teaches us that when 51 is tested for primality using the little Fermat test, there are 2 distinct witnesses w (16 and 35) we could have chosen which would have been Fermat liars, but all 28 other potential co-prime witnesses would have been truth tellers, accurately showing 51 to be composite. Proportionally, little Fermat will have been correct roughly 93% of the time, since only 2 of these 30 possible tests returned a false positive. (I’ll add that for any modulus P, if w<P is not co-prime with the modulus, then the computation mod(w^(P-1),P) will always return a non-unit result, and therefore we can theoretically use any integer w from the set 2:P-2 as a witness. However if w is co-prime with P then P is clearly not prime, and the entire problem becomes a little less interesting. As such, I will only consider co-prime witnesses for this discussion.) Regardless, that would make the little Fermat test for P=51 even more often correct, since it returns the correct result of composite for 46 out of the 48 possible witnesses 2:49. Does this mean Little Fermat is indeed the basis for a good test to rely on to learn if a number is prime? Well, yes. And no.
Little Fermat forms a very good test most of the time, but reliance is a strong word. This means we need to explore the little Fermat test in more depth, focusing on Fermat liars and the case of false positives. To offer some appreciation of the false positive rate, offline, I have tested all composite integers between 4 and 10000, for all their possible co-prime witnesses.
load FermatLiarsData
In that .mat file, I've saved three vectors, X, witnessCount, and liarCount. X is there just to use for plotting purposes and is NaN for all non-composite entries.
whos X witnessCount liarCount
The vector witnessCount is the number of valid witnesses for the corresponding number in X. Corresponding to that is the vector liarCount, which is the number of Fermat liars I found for each composite in X.
How many Fermat test useful witnesses are there for any integer X? This is just 2 less than the number of coprimes of X. The number of coprimes is given by the Euler totient function, commonly called phi(X). (I’ll be going into more depth on the totient in the next chapter of this series, because the Euler totient is a crucial part of understanding how all of this works.)
The witness count is phi(X)-2. Why subtract 2? 1 can never be a witness, but 1 is technically coprime to everything. The same applies to X-1 (which is congruent to -1 mod X.) As such, there are phi(X)-2 coprimes to consider. (I've posted a function called totient on the FEX, but it is easily computed if you know the factorization of X. Or for small numbers, you can just use GCD to identify all co-primes, and count them.)
plot(X,witnessCount,'.')
From that plot, you can learn a few interesting things. (As a mathematician, this is what I love the most, thus to look at whay may be the simplest, most boring plot, and try to find something of value, something I had never thought of before.) For example, we know that when X is prime, then everything from the set 2:X-2 is a valid witness. So the upper boundary on that plot will be the line y==x. As well, there are a few numbers where the order of the set of witnesses will be close to the maximum possible. For example, 961=31*31, has 928 valid witnesses. That makes some sense, as 961 is the square of a prime (31), so we know 961 is divisible only by 31. Only multiples of 31 will not be coprime with 961.
But how about the lower boundary? The least number of valid witnesses will always come from highly composite numbers, because they will share common factors with almost everything. For example 30 = 2*3*5, or 210=2*3*5*7.
witnessCount([30 210 420])
A good discussion about the lower bound for that plot can be found here:
What really matters to us though, is the fraction of the useful witnesses for a little Fermat test that yield a false positive.
plot(X,liarCount./witnessCount,'b.')
Cind = liarCount == witnessCount;
hold on
plot(X(Cind),1,'ro')
ylabel('Liar fraction')
xlabel('X')
title('Fermat pseudo-prime strength')
hold off
A look at this plot shows seven circles in red, corresponding to X from the list {561, 1105, 1729, 2465, 2821, 6601, 8911} which are collectively known as Carmichael numbers. These are numbers where all witnesses return a false positive. Carmichael numbers are themselves fairly rare. You can find a list of them as sequence A002997 in the OEIS. And for those of you who have never wandered around the OEIS, please take this opportunity to do so now. The OEIS stands for Online Encyclopedia of Integer Sequences. It contains a wealth of interesting knowledge about integers and integer sequences.)
There are a few other interesting numbers we can find in that plot, like 91 and 703, where roughly 50% of the valid witnesses yield false positives. Of the complete set, which numbers did return at least a 25% false positive rate for primality? These numbers would be known as strong pseudo-primes for the little Fermat test, because they are pseudo-primes for at least 25% of the potential witnesses. These strong pseudo-primes have some interesting similarities to the Carmichael numbers. (My next post will go into more depth on Carmichael numbers and strong pseudo-primes. At the moment, I am merely interested in looking at the how often the little Fermat test fails overall.)
find(liarCount./witnessCount> 0.25)
You should notice the spacing between successive strong Fermat pseudo-primes is growing slowly, with a spacing of roughly 800 on average in the vicinity of 10000. If I step out beyond by a factor of 10, the next strong Fermat pseudo-primes after 1e5 are {101101, 104653, 107185, 109061, 111361, 114589, 115921 126217, 126673}, so that average spacing is definitely growing.
Given that set, now we can look at the prime factorizations of each of those strong pseudo-primes. Can we learn something about them?
arrayfun(@factor,find(liarCount./witnessCount> 0.25),'UniformOutput',false)
Perhaps the most glaring thing I see in that set of factors is almost all of those strong Fermat pseudo-primes are square free. That is, in that list, only 45=3*3*5 had any replicated factor at all. That property of being square free is something we will see is necessary to be a Carmichael number, but it also suggests that a simple roughness test applied in advance would have eliminated almost all of those strong pseudo-primes as obviously not prime, even at a very low level of roughness.
In fact, for most composite integers, most witnesses do indeed return a negative, indicating the number is not prime, and therefore composite. Little Fermat does not commonly tell falsehoods, even though it can do so.
semilogy(X,movmedian(liarCount./witnessCount,20,'omitnan'),'b-')
title('False positive fraction for composites')
yline(0.0003,'r')
We can learn from this last plot that as the number to be tested grows large, the median false positive rate for little Fermat, even for X as low as only 10000, is roughly 0.0003. (It continues to decrease for larger X too. In fact, I’ve read that when X is on the order of 2^256, the relative fraction of Fermat liars is on the order of 1 in 1e12, and it continues to decrease as X grows in magnitude. In my eyes, that seems pretty good for an imperfect test. Not perfect, but not bad when paired with roughness and perhaps a second little Fermat test using a different witness, and we will start to see tests which bear a higher degree of strength.)
I’ll stop at this point in this post because the post is getting lengthy. In my next post, I’d like to visit some questions about what are Carmichael numbers, about whether some witnesses are better than others, and if there are any numbers which lack any Fermat liars. However, in order to dive more deeply, I will need to explain how/why/when the little Fermat test works, and what causes Fermat liars. Stay tuned, because this starts to get interesting.
If you have published add-ons on File Exchange, you may have noticed that we recently added a new, unique package name field to all add-ons. This enables future support for automated installation with the MATLAB Package Manager. This name will be a unique identifier for your add-on and does not affect the existing add-on title, any file names, or the URL of your add-on.
📝 Update and review until April 10
We generated default package names for all add-ons. You can review and update the package name for your add-ons until April 10, 2026. Review your package names now:
After April 10, you will need to create a new version to change your package name.
🚀 More changes coming with the MATLAB R2026b prerelease
Starting with the MATLAB R2026b prerelease, these package names will take effect. At that time, the package name may appear on the File Exchange page for your add-on.
Keep your eyes peeled for exciting changes coming soon to your add-ons on File Exchange!
Cantera is an open-source suite of tools for problems involving chemical kinetics, thermodynamics, and transport processes. Dr. Su Sun, a recent graduate from Northeastern Chemical Engineering Ph.D. program made significant contributions to MATLAB interface for Cantera in Cantera Release 3.2.0 in collaboration with Dr. Richard West, other Cantera developers, and MathWorks Advanced Support and Development Teams. As part of this Release, MATLAB interface for Cantera transitioned to using the new MATLAB- C++ interface and expanded their unit testing. Further information is available here.
I began coding in MATLAB less than 2 months ago for a class at community college. Alongside the course content, I also completed the MATLAB onramp and introduction to linear algebra self-paced online courses. I think this is the most fun I've had coding since back when I used to make Scratch projects in elementary school. I'm kind of curious if I could recreate some of my favorite childhood Scratch games here.
Anyways, I just wanted to introduce myself since I plan to be really active this year. My name is Mehreen (meh like the meh emoji from the Emoji movie, reen like screen), I'm a data science undergrad sophomore from the U.S. and it's nice to meet you!
Hi everyone,
Some of you may remember my earlier post. Quick version: I'm a biomed PhD student, I use MATLAB daily, and I noticed that AI coding tools often suggest functions that don't exist in R2025b or use deprecated ones. So I built skills that teach them what actually works.
v2.0 adds 54 template `.m` scripts, rewrites all knowledge cards based on blind testing, and verifies every function call against live MATLAB. I tested each skill on 17 prompts and caught 8 hallucinated functions across 5 toolboxes (Medical Imaging, Deep Learning, Image Processing, Stats-ML, Wavelet).
Give it a spin!
Repo: matlab-toolbox-skills
The skills follow the Agent Skills open standard, so they also work with Codex, Gemini CLI, Claude Code and others. If you use the official Matlab MCP Server from MathWorks, these skills complement it: the MCP server executes your code, the skills help the AI write good code to begin with.
One ask
How do we measure performance and evaluate agent skills? We can run blind tests and catch hallucinated functions, but that only covers what we thought to test. The honest answer is that the best way to evaluate these is community consensus and real-world testimonials. How are you using them? What worked? What still broke?
Your use cases and feedback are the most reliable eval I can get, and as a student building this, they're also the real motivation for me to keep going. If a skill saved you from a hallucinated function or pointed you to the right function call, I'd love to hear about it. If something is still wrong, I need to hear about it.
Issues, PRs, or just a reply here. Star the repo if it saved you time.
Thanks!
Happy Spring! and Happy Coding in Matlab!
Best,
Ritish

A Live Script can be converted to an HTML5 framework web application with AI as described in Double Pendulum Chaos Explorer: From HTML5 Prototype to MATLAB interactive application with AI. I have recently provides converted the Live Script Classical Stark Effect to a web application supporting a 3D twirlable display of motion of a particle subject to an inverse square law force plus an additional constant force - the problem known as the classical Stark effect.
The web application deployed to GitHub may be launched here and documents its dependencies below the interactive application. The files are available at Classical Stark Effect — Interactive Web Simulation. One gotcha was the need to enable hardware acceleration in Chrome (no problem in Safari) to support a 3D twirlable display. If hardware acceleration is disabled in Chrome, the application provides a warning and replaces the 3D twirlable display with a 2D alternate.
The conversion of the script to a web application was performed with Perplexity.ai. The GitHub deployment was accomplished with Anthropic's Claude using the open source GitHub CLI. WIth the gh CLI (already installed and authenticated on my Mac) via osascript, and Claude connected to my file system via MCP and an ngrok server, Claude executed on my Mac the following sequence of steps:
1. git init
Creates a hidden .git/ directory in the staging folder, initializing it as a local git repository. Before this command the folder is just a plain directory; after it, git can track files there. Run once per new project.
2. git branch -M main
Renames the default branch to main. Older git versions default to master; GitHub now expects main. The -M flag forces the rename even if main already exists. Must run after git init and before the first commit.
3. git add -A
Stages all files in the directory tree for the next commit. The -A flag means "all" -- new files, modified files, and deleted files are all included. This does not write anything to GitHub; it only updates git's internal index (the staging area) on your local machine.
4. git commit -m 'Initial release: Classical Stark Effect Interactive Simulation'
Takes everything in the staging area and freezes it into a permanent commit object stored in .git/. This is the snapshot that will be pushed. The -m flag provides the commit message inline. After this command, git knows exactly what files exist and what their contents are -- gh repo create --push will send exactly this snapshot.
5. gh repo create ClassicalStarkEffect --public --source=. --push
Three things happen in sequence inside this one command:
- gh repo create ClassicalStarkEffect --public -- calls the GitHub API to create a new empty public repository named ClassicalStarkEffect under the authenticated account (DuncanCarlsmith).
- --source=. -- tells gh to treat the current directory as the local git repo. It reads .git/ to find the commits and configures the remote.
- --push -- sets the new GitHub repo as origin and runs the equivalent of git push origin main, sending the commit from step 4 up to GitHub.
Without steps 1-4 having run first, --push would have nothing to send and the repo would land empty.
6. gh api repos/DuncanCarlsmith/ClassicalStarkEffect/pages --method POST -f build_type=legacy -f source[branch]=main -f 'source[path]=/'
Calls the GitHub REST API directly to enable GitHub Pages on the repo. Breaking down the flags:
- --method POST -- this is a create operation (not a read), so it uses HTTP POST.
- -f build_type=legacy -- critical flag. Tells GitHub to serve files directly from the branch. The alternative (workflow) would expect a .github/workflows/ Actions file to build and deploy the site, which doesn't exist here, and would produce a permanent 404.
- -f source[branch]=main -- serve from the main branch.
- -f 'source[path]=/' -- serve from the root of the branch (as opposed to a /docs subdirectory).
This is the API equivalent of going to Settings > Pages in the GitHub web UI and setting Branch: main, Folder: / (root), clicking Save.
7. curl -s -o /dev/null -w "%{http_code}" https://duncancarlsmith.github.io/ClassicalStarkEffect/
Not a git or gh command, but the verification step. GitHub Pages takes ~60 seconds to build after step 6. This curl fetches the live URL and prints only the HTTP status code (-w "%{http_code}"), discarding the body (-o /dev/null) and suppressing progress output (-s). 200 means live; 404 means still building.
An emirp is a prime that is prime when viewed in in both directions. They are not too difficult to find at a lower level. For example...
isprime([199 991])
Gosh, that was easy. But what happens if the number is a bit larger? The problem is, primes themselves tend to be rare on the number line when you get into thousands or tens of thousands of decimal digits. And recently, I read that a world record size prime had been found in this form. You have probably all heard of Matt Parker and numberphile.
And so, I decided that MATLAB would be capable of doing better. Why not? After all, at the time, the record size emirp had only 10002 decimal digits.
How would I solve this problem? First, we can very simply write a potential emirp as
10^n + a
then we can form the flipped version as
ahat*10^(n-d) + 1
where ahat is the decimally flipped version of a, and d is chosen based on the number of decimal digits in the number a itself. Not all emirps will be of that form of course, but using all of those powers of 10 makes it easy to construct a large number and its reversed form. And that is a huge benefit in this. For example,
Pfor = sym(10)^101 + 943
Prev = 349*sym(10)^99 + 1
It is easier to view these numbers using a little code I wrote, one that redacts most of those boring zeros.
emirpdisplay(Pfor)
emirpdisplay(Prev)
And yes, they are both prime, and they both have 102 decimal digits.
isprime([Pfor,Prev])
Sadly, even numbers that large are very small potatoes, at least in the world of large primes. So how do we solve for a much larger prime pair using MATLAB?
The first thing I want to do is to employ roughness at a high level. If a number is prime, then it is maximally rough. (I posted a few discussions about roughness some time ago.)
https://www.mathworks.com/matlabcentral/discussions/tips/879745-primes-and-rough-numbers-basic-ideas
In this case, I'm going to look for serious roughness, thus 2e9-rough numbers. Again, a number is k-rough if its smallest prime factor is greater than k. There are roughly 98 million primes below 2e9.
The general idea is to compute the remainders of 10^12345, modulo every prime in that set of primes below 2e9. This MUST be done using int64 or uint64 arithmetic, as doubles will start to fail you above
format short g
sqrt(flintmax)
The sqrt is in there because we will be multiplying numbers together here, and we need always to stay below intmax for the integer format you are working with. However, if we work in an integer format, we can get as high as 2e9 easily enough, by going to int64 or uint64.
sqrt(double(intmax('int64')))
And, yes, this means I could have gone as high as primes(3e9), however, I stopped at 2e9 due to the amount of RAM on my computer. 98 million primes seemed enough for this task. And even then, I found myself working with all of the cores on my computer. (Note that I found int64 arithmetic will only fire up the performance cores on your Mac via automatic multi-threading. Mine has 12 performance cores, even though it has 16 total cores.)
I computed the remainders of 10^12345 with respect to each prime in that set using a variation of the powermod algorithm. (Not powermod itself, which was itself not sufficiently fast for my purposes.) Once I had those 98 millin remainders in a vector, then it became easy to use a variation of the sieve of Eratosthenes to identify 2e9-rough numbers.
For example, working at 101 decimal digits, if I search for primes of the form 10^101+a, with a in the interval [1,10000], there are 256 numbers of that form which are 2e9-rough. Roughness is a HUGE benefit, since as you can see here, I would not want to test for primality all 10000 possible integers from that interval.
Next, I flip those 256 rough numbers into their mirror image form. Which members of that set are also rough in the mirror image form? We would then see this further reduces the set to only 34 candidates we need test for primality which were rough in both directions. With now only a few direct tests for primality, we would find that pair of 102 digit primes shown above.
Of course, I'm still needing to work with primes in the regime of 10000 plus decimal digits, and that means I need to be smarter about how I test a number to be prime. The isprime test given by sym/isprime only survives out to around 1000 decimal digits before it starts to get too slow. That means I need to perform Fermat tests to screen numbers for primality. If that indicates potential primality, I currently use a Miller-Rabin code to verify that result, one based on the tool Java.Math.BigInteger.isProbablePrime.
And since Wikipedia tells me the current world record known emirp was
117,954,861 * 10^11111 + 1 discovered by Mykola Kamenyuk
that tells me I need to look further out yet. I chose an exponent of 12345, so starting at 10^12345. Last night I set my Mac to work, with all cores a-fumbling, a-rumbling at the task as I slept. Around 4 am this morning, it found this number:
emirp = @(N,a) sym(10)^N + a;
Pfor = emirp(12345,10519197);
Prev = sym(flip(char(Pfor)));
emirpdisplay(Pfor)
emirpdisplay(Prev)
isProbablePrimeFLT([Pfor,Prev],210)
I'm afraid you will need to take my word for it that both also satisfy a more robust test of primality, as even a Miller-Rabin test that will take more time than the MATLAB version we get for use in a discussion will allow. As far as a better test in the form of the MATLAB isprime utility to verify true primality, that test is still running on my computer. I'll check back in a few hours to see if it fininshed.
Anyway, the above numbers now form the new world record known emirp pair, at 12346 decimal digits. Yes, I do recognize this is still what I would call low hanging fruit, that having announced a largest prime of this form, someone else willl find one yet larger in a few weeks or months. But even so, for the moment, MATLAB owns the world record!
If anyone else wants a version of the codes I used for the search, I've attached a version (emirpsearchpar.m) that employs the parallel processing toolbox. I do have as well a serial version which is of course, much, much slower. It would be fun to crowd source a larger world record yet from the MATLAB community.
Hey folks in MATLAB community! I'm an engineering student from India messing around with deep learning/ML for spotting faults in power electronics stuff—like inverter issues or microgrid glitches in Simulink.
What's your take?
- Which toolbox rocks for this—Deep Learning one or Predictive Maintenance?
- Any gotchas when training on sim data vs real hardware?
- Cool workflows or GitHub links you've used?
Would love your real experiences! 😊
The "Issues" with Constant Properties
MATLABs Constant properties can be rather difficult to deal with at times. For those unfamiliar there are two distinct behaviors when accessing constant properties of a class. If a "static" pattern is used ClassName.PropName the the value, as it was assigned to the property is returned; that is to say that you will have a nargout of 1. But, rather frustratingly, if an instance of the class is used when accessing the constant property, such as ArrayOfClassName.PropName then your nargout will be equivalent to the number of elements in the array; this means that functionally the constant property accessing scheme is identical to that of the element wise properties you find on "instance" properties.
Motivation for Correcting Constant Property Behavior
This can be frustraing since constant properties are conceptually designed to tie data to a class. You could see this design pattern being useful where a super class were to define an abstract constant property, that would drive the behavior of subclasses; the subclasses define the value of the property and the super class uses it. I would like to use this design to develop a custom display "MatrixDisplay" focused mixin (like the internal matlab.mixin.internal.MatrixDisplay). The idea is that missing element labels, invalid handle element labels, and other semantic values can be configured by the subclasses, conveniently just by setting the constant properties; these properties will be used by the super class to substitute the display strings of appropriate elements. Most of the processing will happen within the super class as to enable simple, low-investment, opt-in display options for array style classes.
The issue is that you can not rely on constant property access to return the appropriate value when the instance you've been passed is empty. This also happens with excessive outputs when the instance is non-scalar, but those extra values from the CSL are just ignored, while Id imagine there is an effect on performance from generating the excess outputs (assuming theres no internal optimization for the unused outputs), this case still functions appropriately. As I enjoying exploring MATLAB, I found an internal indexing mixing class in the past that provides far greater control of do indexing; I've done a good deal of neat things with it, though at the cost of great overhead when getting implementing cool "proof of concept/showcase" examples. Today I used it to quickly implement a mix in that "fixes" constant properties such that they always return as though they were called statically from the class name, as opposed to an instance.
A Simplistic Solution
To do this I just intercepted the property indexing, checked if it was constant, and used the DefaultValue property of the metadata to return the value. This works nicely since we are required to attempt to initialize a "dummy" scalar array, or generate a function handle; both of those would likely be slower, and in the former case, may not be possible depending on the subclass implementation. It is worth noting that this method of querying the value from metadata is safe because constant properties are immutable and thus must be established as the class is loaded. Below is the small utility class I have implemented to get predictable constant variable access into classes that benefit from it. Lastly it is worth noting that I've not torture testing the rerouting of the indexing we aren't intercepting, in my limited play its behaved as expected but it may be worth looking over if you end up playing around with this and notice abnormal property assignment or reading from non-constant properties.
Sample Class Implementation
classdef(Abstract, HandleCompatible) ConstantProperty < matlab.mixin.internal.indexing.RedefinesDotProperties
%ConstantProperty Returns instance indexed constant properties as though they were statically indexed.
% This class overloads property access to check if the indexed property is constant and return it properly.
%% Property membership utility methods
methods(Access=private)
function [isConst, isProp] = isConstantProp(obj, prop, options)
%isConstantProp Determine if the input property names are constant properties of the input
arguments
obj mixin.ConstantProperty;
prop string;
options.Flatten (1, 1) logical = false;
end
% Store the cache to avoid rechecking string membership and parsing metadata
persistent class_cache
% Initialize cache for all subclasses to maintain their own caches
if(isempty(class_cache))
class_cache = configureDictionary("string", "dictionary");
end
% Gather the current class being analyzed
classname = string(class(obj));
% Check if the current class has a cache, if not make one
if(~isKey(class_cache, classname))
class_cache(classname) = configureDictionary("string", "struct");
end
% Alias the current classes cache
prop_cache = class_cache(classname);
% Check which inputs are already cached
isCached = isKey(prop_cache, prop);
% Add any values that have yet to be cached to the cache
if(any(~isCached, "all"))
% Flatten cache additions
props = row(prop(~isCached));
% Gather the meta-property data of the input object and determine if inputs are listed properties
mc_props = metaclass(obj).PropertyList;
% Determine which properties are keys
[isConst, idx] = ismember(props, string({mc_props.Name}));
idx = idx(isConst);
% Check which of the inputs are constant properties
isConst = repmat(isConst, 2, 1);
isConst(1, isConst(1, :)) = [mc_props(idx).Constant];
% Parse the results into structs for caching
cache_values = cell2struct(num2cell(isConst), ["isConst"; "isProp"]);
prop_cache(props) = row(cache_values);
% Re-sync the cache
class_cache(classname) = prop_cache;
end
% Extract results from the cache
values = prop_cache(prop);
if(options.Flatten)
% Split and reshape output data
sz = size(prop);
isConst = reshape(values.isConst, sz);
isProp = reshape(values.isProp, sz);
else
isConst = struct2cell(values);
end
end
function [isConst, isProp] = isConstantIdxOp(obj, idxOp)
%isConstantIdxOp Determines if the idxOp is referencing a constant property.
arguments
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
end
import matlab.indexing.IndexingOperationType;
if(idxOp(1).Type == IndexingOperationType.Dot)
[isConst, isProp] = isConstantProp(obj, idxOp(1).Name);
else
[isConst, isProp] = deal(false);
end
end
function A = getConstantProperty(obj, idxOp)
%getConstantProperty Returns the value of a constant property using a static reference pattern.
arguments
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
end
A = findobj(metaclass(obj).PropertyList, "Name", idxOp(1).Name).DefaultValue;
end
end
%% Dot indexing methods
methods(Access = protected)
function A = dotReference(obj, idxOp)
arguments(Input)
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
end
arguments(Output, Repeating)
A
end
% Force at least one output
N = max(1, nargout);
% Check if the indexing operation is a property, and if that property is constant
[isConst, isProp] = isConstantIdxOp(obj, idxOp);
if(~isProp)
% Error on invalid properties
throw(MException( ...
"JB:mixin:ConstantProperty:UnrecognizedProperty", ...
"Unrecognized property '%s'.", ...
idxOp(1).Name ...
));
elseif(isConst)
% Handle forwarding indexing operations
if(isscalar(idxOp))
% Direct assignment
[A{1:N}] = getConstantProperty(obj, idxOp);
else
% First extract constant property then forward indexing operations
tmp = getConstantProperty(obj, idxOp);
[A{1:N}] = tmp.(idxOp(2:end));
end
else
% Handle forwarding indexing operations
if(isscalar(idxOp))
% Unfortunately we can't just recall obj.(idxOp) to use default/built-in so we manually extract
[A{1:N}] = obj.(idxOp.Name);
else
% Otherwise let built-in handling proceed
tmp = obj.(idxOp(1).Name);
[A{1:N}] = tmp.(idxOp(2:end));
end
end
end
function obj = dotAssign(obj, idxOp, values)
arguments(Input)
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
end
arguments(Input, Repeating)
values
end
% Handle assignment based on presence of forward indexing
if(isscalar(idxOp))
% Simple broadcasted assignment
[obj.(idxOp.Name)] = deal(values{:});
else
% Initialize the intermediate values and expand the values for assignment
tmp = {obj.(idxOp(1).Name)};
[tmp.(idxOp(2:end))] = deal(values{:});
% Reassign the modified data to the output object
[obj.(idxOp(1).Name)] = deal(tmp{:});
end
end
function n = dotListLength(obj, idxOp, idxCnt)
arguments(Input)
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
idxCnt (1, :) matlab.indexing.IndexingContext;
end
if(isConstantIdxOp(obj, idxOp))
if(isscalar(idxOp))
% Constant properties will also be 1
n = 1;
else
% Checking forwarded indexing operations on the scalar constant property
n = listLength(obj.(idxOp(1).Name), idxOp(2:end), idxCnt);
end
else
% Check the indexing operation normally
% n = listLength(obj, idxOp, idxCnt);
n = numel(obj);
end
end
end
end
MATLAB MCP Core Server v0.6.0 has been released onGitHub: https://github.com/matlab/matlab-mcp-core-server/releases/tag/v0.6.0
Release highlights:
- New cross-platform MCP Bundle; one-click installation in Claude Desktop
Enhancements:
- Provide structured output from check_matlab_code and additional information for MATLAB R2022b onwards
- Made project_path optional in evaluate_matlab_code tool for simpler tool calls
- Enhanced detect_matlab_toolboxes output to include product version
Bug fixes:
- Updated MCP Go SDK dependency to address CVE.
We encourage you to try this repository and provide feedback. If you encounter a technical issue or have an enhancement request, create an issue https://github.com/matlab/matlab-mcp-core-server/issues
k-Wave is a MATLAB community toolbox with a track record that includes over 2,500 citations on Google Scholar and over 7,500 downloads on File Exchange. It is built for the "time-domain simulation of acoustic wave fields" and was recently highlighted as a Pick of the Week.
In December, release v1.4.1 was published on GitHub including two new features led by the project's core contributors with domain experts in this field. This first release in several years also included quality and maintainability enhancements supported by a new code contributor, GitHub user stellaprins, who is a Research Software Engineer at University College London. Her contributions in 2025 spanned several software engineering aspects, including the addition of continuous integration (CI), fixing several bugs, and updating date/time handling to use datetime. The MATLAB Community Toolbox Program sponsored these contributions, and welcomes to see them now integrated into a release for k-Wave users.
I'd like to share some work from Controls Educator and long term collabortor @Dr James E. Pickering from Harper Adams University. He is currently developing a teaching architecture for control engineering (ACE-CORE) and is looking for feedback from the engineering community.
ACE-CORE is delivered through ACE-Box, a modular hardware platform (Base + Sense, Actuate). More on the hardware here: What is the ACE-Box?
The Structure
(1) Comprehend
Learners build conceptual understanding of control systems by mapping block diagrams directly to physical components and signals. The emphasis is on:
- Feedback architecture
- Sensing and actuation
- Closed-loop behaviour in practical terms
(2) Operate
Using ACE-Box (initially Base + Sense), learners run real closed-loop systems. The learners measure, actuate, and observe real phenomena such as: Noise, Delay, Saturation
Engineering requirements (settling time, overshoot, steady-state error, etc.) are introduced explicitly at this stage.
After completing core activities (e.g., low-pass filter implementation or PID tuning), the pathway branches (see the attached diagram)
(3a) Refine (Option 1) Students improve performance through structured tuning:
- PID gains
- Filter coefficients
- Performance trade-offs
The focus is optimisation against defined engineering requirements.
(3b) Refine → Engineer (Option 2)
Modelling and analytical design become more explicit at this stage, including:
- Mathematical modelling
- Transfer functions
- System identification
- Stability analysis
- Analytical controller design
Why the Branching?
The structure reflects two realities:
- Engineers who operate and refine existing control systems
- Engineers who design control systems through mathematical modelling
Your perspective would be very valuable:
- Does this progression reflect industry reality?
- Is the branching structure meaningful?
- What blind spots do you see?
Constructive critique is very welcome. Thank you!
Giving All Your Claudes the Keys to Everything

Introduction
This started as a plumbing problem. I wanted to move files between Claude’s cloud container and my Mac faster than base64 encoding allows. What I ended up building was something more interesting: a way for every Claude – Desktop, web browser, iPhone – to control my entire computing environment with natural language. MATLAB, Keynote, LaTeX, Chrome, Safari, my file system, text-to-speech. All of it, from any device, through a 200-line Python server and a free tunnel.
It isn’t quite the ideal universal remote. Web and iPhone Claude do not yet have the agentic Playwright-based web automation capabilities available to Claude desktop via MCP. A possible solution may be explored in a later post.
Background information
The background here is a series of experiments I’ve been running on giving AI desktop apps direct access to local tools. See How to set up and use AI Desktop Apps with MATLAB and MCP servers which covers the initial MCP setup, A universal agentic AI for your laptop and beyond which describes what Desktop Claude can do once you give it the keys, and Web automation with Claude, MATLAB, Chromium, and Playwright which describes a supercharged browser assistant. This post is about giving such keys to remote Claude interfaces.
Through the Model Context Protocol (MCP), Claude Desktop on my Mac can run MATLAB code, read and write files, execute shell commands, control applications via AppleScript, automate browsers with Playwright, control iPhone apps, and take screenshots. These powers have been limited to the desktop Claude app – the one running on the machine with the MCP servers. My iPhone Claude app and a Claude chat launched in a browser interface each have a Linux container somewhere in Anthropic’s cloud. The container can reach the public internet. Until now, my Mac sat behind a home router with no public IP.
An HTTP server MCP replacement
A solution is a small HTTP server on the Mac. Use ngrok (free tier) to give the Mac a public URL. Then anything with internet access – including any Claude’s cloud container – can reach the Mac via curl.
iPhone/Web Claude -> bash_tool: curl https://public-url/endpoint
-> Internet -> ngrok tunnel -> Mac localhost:8765
-> Python command server -> MATLAB / AppleScript / filesystem
The server is about 200 lines of Python using only the standard library. Two files, one terminal command to start. ngrok provides HTTPS and HTTP Basic Auth with a single command-line flag.
The complete implementation consists of three files:
claude_command_server.py (~200 lines): A Python HTTP server using http.server from the standard library. It implements BaseHTTPRequestHandler with do_GET and do_POST methods dispatching to endpoint handlers. Shell commands are executed via subprocess.run with timeout protection. File paths are validated against allowed roots using os.path.realpath to prevent directory traversal attacks.
start_claude_server.sh (~20 lines): A Bash script that starts the Python server as a background process, then starts ngrok in the foreground. A trap handler ensures both processes are killed on Ctrl+C.
iphone_matlab_watcher.m (~60 lines): MATLAB timer function that polls for command files every second.
The server exposes six GET and six POST endpoints. The GET endpoints thus far handle retrieval: /ping for health checks, /list to enumerate files in a transfer directory, /files/<n> to download a file from that directory, /read/<path> to read any file under allowed directories, and / which returns the endpoint listing itself. The POST endpoints handle execution and writing: /shell runs an allowlisted shell command, /osascript runs AppleScript, /matlab evaluates MATLAB code through a file-watcher mechanism, /screenshot captures the screen and returns a compressed JPEG, /write writes content to a file, and /upload/<n> accepts binary uploads.
File access is sandboxed by endpoint. The /read/ endpoint restricts reads to ~/Documents, ~/Downloads, and ~/Desktop and their subfolders. The /write and /upload endpoints restrict writes to ~/Documents/MATLAB and ~/Downloads. Path traversal is validated – .. sequences are rejected.
The /shell endpoint restricts commands to an explicit allowlist: open, ls, cat, head, tail, cp, mv, mkdir, touch, find, grep, wc, file, date, which, ps, screencapture, sips, convert, zip, unzip, pbcopy, pbpaste, say, mdfind, and curl. No rm, no sudo, no arbitrary execution. Content creation goes through /write.
Two endpoints have no path restrictions: /osascript and /matlab. The /osascript endpoint passes its payload to macOS’s osascript command, which executes AppleScript – Apple’s scripting language for controlling applications via inter-process Apple Events. An AppleScript can open, close, or manipulate any application, read or write any file the user account can access, and execute arbitrary shell commands via “do shell script”. The /matlab endpoint evaluates arbitrary MATLAB code in a persistent session. Both are as powerful as the user account itself. This is deliberate – these are the endpoints that make the server useful for controlling applications. The security boundary is authentication at the tunnel, not restriction at the endpoint.
The MATLAB Workaround
MATLAB on macOS doesn’t expose an AppleScript interface, and the MCP MATLAB tool uses a stdio connection available only from Desktop. So how does iPhone Claude run MATLAB?
An answer is a file-based polling mechanism. A MATLAB timer function checks a designated folder every second for new command files. The remote Claude writes a .m file via the server, MATLAB executes it, writes output to a text file, and sets a completion flag. The round trip is about 2.5 seconds including up to 1 second of polling latency. (This could be reduced by shortening the polling interval or replacing it with Java’s WatchService for near-instant file detection.)
This method provides full MATLAB access from a phone or web Claude, with your local file system available, under AI control. MATLAB Mobile and MATLAB Online do not offer agentic AI access.
Alternate approaches
I’ve used Tailscale Funnel as an alternative tunnel to manually control my MacBook from iPhone but you can’t install Tailscale inside Anthropic’s container. An alternative approach would replace both the local MCP servers and the command server with a single remote MCP server running on the Mac, exposed via ngrok and registered as a custom connector in claude.ai. This would give all three Claude interfaces native MCP tool access including Playwright or Puppeteer with comparable latency but I’ve not tried this.
Security
Exposing a command server to the internet raises questions. The security model has several layers. ngrok handles authentication – every request needs a valid password. Shell commands are restricted to an allowlist (ls, cat, cp, screencapture – no rm, no arbitrary execution). File operations are sandboxed to specific directory trees with path traversal validation. The server binds to localhost, unreachable without the tunnel. And you only run it when you need it.
The AppleScript and MATLAB endpoints are relatively unrestricted in my setup, giving Claude Desktop significant capabilities via MCP. Extending them through an authenticated tunnel is a trust decision.
Initial test results
I ran an identical three-step benchmark from each Claude interface: get a Mac timestamp via AppleScript, compute eigenvalues of a 10x10 magic square in MATLAB, and capture a screenshot. Same Mac, same server, same operations.
| Step | Desktop (MCP) | Web Claude | iPhone Claude |
|------|:------------:|:----------:|:-------------:|
| AppleScript timestamp | ~5ms | 626ms | 623ms |
| MATLAB eig(magic(10)) | 108ms | 833ms | 1,451ms |
| Screenshot capture | ~200ms | 1,172ms | 980ms |
| **Total** | **~313ms** | **2,663ms** | **3,078ms** |
The network overhead is consistent: web and iPhone both add about 620ms per call (the ngrok round trip through residential internet). Desktop MCP has no network overhead at all. The MATLAB variance between web (833ms) and iPhone (1,451ms) is mostly the file-watcher polling jitter – up to a second of random latency depending on where in the polling cycle the command arrives.
All three Claudes got the correct eigenvalues. All three controlled the Mac. The slowest total was 3 seconds for three separate operations from a phone. Desktop is 10x faster, but for “I’m on my phone and need to run something” – 3 seconds is fine.
Other tests

MATLAB: Computations (determinants, eigenvalues), 3D figure generation, image compression. The complete pipeline – compute on Mac, transfer figure to cloud, process with Python, send back – runs in under 5 seconds. This was previously impossible; there was no reasonable mechanism to move a binary file from the container to the Mac.
Keynote: Built multi-slide presentations via AppleScript, screenshotted the results.
TeXShop: Wrote a .tex file containing ten fundamental physics equations (Maxwell through the path integral), opened it in TeXShop, typeset via AppleScript, captured the rendered PDF. Publication-quality typesetting from a phone. I don’t know who needs this at 11 PM on a Sunday, but apparently I do.
Safari: Launched URLs on the Mac from 2,000 miles away. Or 6 feet. The internet doesn’t care.
Finder: Directory listings, file operations, the usual filesystem work.
Text-to-speech: Mac spoke “Hello from iPhone” and “Web Claude is alive” on command. Silly but satisfying proof of concept.
File Transfer: The Original Problem, Solved
Remember, this started because I wanted faster file transfers for desktop Claude. Compare:
| Method | 1 MB file | 5 MB file | Limit |
|--------|-----------|-----------|-------|
| Old (base64 in context) | painful | impossible | ~5 KB |
| Command server upload | 455ms | ~3.6s | tested to 5 MB+ |
| Command server download | 1,404ms | ~7s | tested to 5 MB+ |
The improvement is the difference between “doesn’t work” and “works in seconds.”
Cross-Device Memory
A nice bonus: iPhone and web Claudes can search Desktop conversations using the same memory tools. Context established in a Desktop session – including the server password – can be retrieved from a phone conversation. You do have to ask explicitly (“search my past conversations for the command server”) or insert information into the persistent context using settings rather than assuming Claude will look on its own.
Web Claudes
The most surprising result was web Claude. I expected it to work – same container infrastructure, same bash_tool, same curl. But “expected to work” and “just worked, first try, no special setup” are different things. I opened claude.ai in a browser, gave it the server URL and password, and it immediately ran MATLAB, took screenshots, and made my Mac speak. No configuration, no troubleshooting, no “the endpoint format is wrong” iterations. If you’re logged into claude.ai and the server is running, you have full Mac access from any browser on any device.
What This Means
MCP gave Desktop Claude the keys to my Mac. A 200-line server and a free tunnel duplicated those keys for every other Claude I use. Three interfaces, one Mac, all the same applications, all controlled in natural language.
Additional information
An appendix gives Claude’s detailed description of communications. Codes and instructions are available at the MATLAB File Exchange: Giving All Your Claudes the Keys to Everything.
Acknowledgments and disclaimer
The problem of file transfer was identified by the author. Claude suggested and implemented the method, and the author suggested the generalization to accommodate remote Claudes. This submission was created with Claude assistance. The author has no financial interest in Anthropic or MathWorks.
—————
Appendix: Architecture Comparison
Path A: Desktop Claude with a Local MCP Server
When you type a message in the Desktop Claude app, the Electron app sends it to Anthropic’s API over HTTPS. The LLM processes the message and decides it needs a tool – say, browser_navigate from Playwright. It returns a tool_use block with the tool name and arguments. That block comes back to the Desktop app over the same HTTPS connection.
Here is where the local MCP path kicks in. At startup, the Desktop app read claude_desktop_config.json, found each MCP server entry, and spawned it as a child process on your Mac. It performed a JSON-RPC initialize handshake with each one, then called tools/list to get the tool catalog – names, descriptions, parameter schemas. All those tools were merged and sent to Anthropic with your message, so the LLM knows what’s available.
When the tool_use block arrives, the Desktop app looks up which child process owns that tool and writes a JSON-RPC message to that process’s stdin. The MCP server (Playwright, MATLAB, whatever) reads the request, does the work locally, and writes the result back to stdout as JSON-RPC. The Desktop app reads the result, sends it back to Anthropic’s API, the LLM generates the final text response, and you see it.
The Desktop app is really just a process manager and JSON-RPC router. The config file is a registry of servers to spawn. You can plug in as many as you want – each is an independent child process advertising its own tools. The Desktop app doesn’t care what they do internally. The MCP execution itself adds almost no latency because it’s entirely local, process-to-process on your Mac. The time you perceive is dominated by the two HTTPS round-trips to Anthropic, which all three Claude interfaces share equally.
Path B: iPhone Claude with the Command Server
When you type a message in the Claude iOS app, the app sends it to Anthropic’s API over HTTPS, same as Desktop. The LLM processes the message. It sees bash_tool in its available tools – provided by Anthropic’s container infrastructure, not by any MCP server. It decides it needs to run a curl command and returns a tool_use block for bash_tool.
Here, the path diverges completely from Desktop. There is no iPhone app involvement in tool execution. Anthropic routes the tool_use to a Linux container running in Anthropic’s cloud, assigned to your conversation. This container is the “computer” that iPhone Claude has access to. The container runs the curl command – a real Linux process in Anthropic’s data center.
The curl request goes out over the internet to ngrok’s servers. ngrok forwards it through its persistent tunnel to the claude_command_server.py process running on localhost on your Mac. The command server authenticates the request (basic auth), then executes the requested operation – in this case, running an AppleScript via subprocess.run(['osascript', ...]). macOS receives the Apple Events, launches the target application, and does the work.
The result flows back the same way: osascript returns output to the command server, which packages it as JSON and sends the HTTP response back through the ngrok tunnel, through ngrok’s servers, back to the container’s curl process. bash_tool captures the output. The container sends the tool result back to Anthropic’s API. The LLM generates the text response, and the iPhone app displays it.
The iPhone app is a thin chat client. It never touches tool execution – it only sends and receives chat messages. The container does the curl work, and the command server bridges from Anthropic’s cloud to your Mac. The LLM has to know to use curl with the right URL, credentials, and JSON format. There is no tool discovery, no protocol handshake, no automatic routing. The knowledge of how to reach your Mac is carried entirely in the LLM’s context.
—————
Duncan Carlsmith, Department of Physics, University of Wisconsin-Madison. duncan.carlsmith@wisc.edu


