Mostly HarmlessFueled by lemons.https://jlowin.dev/👋 Hello, world!https://jlowin.dev/blog/hello-world/https://jlowin.dev/blog/hello-world/Fri, 13 Sep 2024 00:00:00 GMT<p>&lt;div class="flex justify-center"&gt; &lt;iframe width="560" height="315" src="https://www.youtube.com/embed/ZS90l4L2t6k?si=cf_WiB6Ji3hCxEun" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt; &lt;/div&gt;</p> <p>&lt;p class="text-center"&gt; (Hello, reader! You probably want &lt;a href='the-qualified-self'&gt;this post&lt;/a&gt; instead.) &lt;/p&gt;</p> Teaching AI to Label GitHub Issueshttps://jlowin.dev/blog/ai-labeler/ai-labeler/https://jlowin.dev/blog/ai-labeler/ai-labeler/A label of loveSat, 02 Nov 2024 00:00:00 GMT<p>import { Image } from 'astro:assets'; import slackScreenshot from './slack-screenshot.png';</p> <p>&lt;Image src={slackScreenshot} alt="Slack Screenshot" class="shadow-lg p-6 mx-auto" width="500"/&gt;</p> <blockquote> <p>"Is GitHub using AI to label PRs now?"</p> </blockquote> <p>When my colleague Nate asked me this question in Slack, I had to pause. "I don't think so?" And then: "They should though."</p> <p><strong>tl;dr: They don't – but you can!</strong></p> <p>I built a GitHub Action that uses LLMs to intelligently label your issues and PRs, and you can drop it into your repos <a href="https://github.com/marketplace/actions/ai-labeler">right now</a>.</p> <p>Beyond just announcing a new tool, I want to share a little about what I learned about practical AI application design, the surprising effectiveness of structured reasoning with smaller models, and why I believe this represents a perfect case study in augmenting (rather than replacing) human workflows.</p> <h2>Why Labels Matter</h2> <p>As an open-source contributor, you've surely seen labels on issues and PRs: colorful tags that categorize work in meaningful ways.</p> <p>As a maintainer, labels represent a fairly sophisticated system for repository orchestration. Similar to hashtags in early social media, labels are an extremely simple construct that have nonetheless transcended their original purpose and become a critical tool for open-source management. They:</p> <ul> <li>shape contributor behavior (<code>good first issue</code>, <code>help wanted</code>)</li> <li>set expectations (<code>breaking change</code>, <code>duplicate</code>, <code>needs-mre</code>)</li> <li>route attention (<code>security-review</code>, <code>needs-reproduction</code>, <code>frontend</code>)</li> <li>advertise features and milestones (<code>enhancement</code>, <code>caching</code>, <code>RBAC</code>, <code>3.x</code>)</li> <li>and drive automation (many human and automated workflows use labels as triggers or status indicators)</li> </ul> <p>One of my favorite examples is the <code>great writeup</code> label <a href="https://github.com/PrefectHQ/prefect/issues?q=sort%3Aupdated-desc+is%3Aopen+label%3A%22great+writeup%22">on the Prefect repo</a>, which highlights issues or resolutions that are exceptionally well-written. It's a great way to recognize and encourage good contributor experiences, and it's a powerful "show don't tell" signal for new contributors.</p> <p>Most labeling today falls into two categories: manual application (time-consuming and inconsistent) or static automation based on simple rules. The excellent first-party <a href="https://github.com/marketplace/actions/labeler">Pull Request Labeler action</a>, for instance, can apply path-based rules such as adding the <code>frontend</code> label to any PR that touching files in <code>ui/**</code>. In fact, it was seeing this <em>deterministic</em> behavior that prompted Nate's question in the first place.</p> <p>But path-based labeling can't tell you whether those changes need security review, or if they're breaking existing APIs, or if they'd make a great first issue for new contributors. To automate labeling effectively, we need something that can actually understand content, intent, and context.</p> <p>Luckily, I know a guy.</p> <h2>Labels, Meet AI</h2> <p>LLMs are a perfect fit for this problem. Classification, or mapping unstructured context onto a set of categories, is one of their most fundamental operations! Most importantly, they can understand the context and even the intent behind any changes, not just their objective surface characteristics. They can tell when a PR constitutes an enhancement, when test coverage is insufficient, or when a security review is needed.</p> <p><strong>So... let's build an AI labeler!</strong></p> <p>Using <a href="https://controlflow.ai">ControlFlow</a>, the core implementation is surprisingly simple. In fact, despite representing the entirety of this action's "magic", I spent only a fraction of my time orchestrating the AI logic and all the rest trying to get the action itself to work in CI.</p> <p>You may draw your own conclusions about the state of developer happiness.</p> <p>Here is a slightly simplified version of the core code. In this flow:</p> <pre><code>import controlflow as cf from pydantic import BaseModel from typing import Optional, Union @cf.flow def labeling_workflow( pr_or_issue: Union["PullRequest", "Issue"], labels: list["Label"], ) -&gt; list[str]: class Reasoning(BaseModel): label_name: str reasoning: str should_apply: bool labeler = cf.Agent( name="GitHub Labeler", model="openai/gpt-4o-mini", instructions="You are an expert at labelling GitHub issues and PRs.", ) decision = cf.run( "Consider the PR/issue and reason about potential labels", result_type=list[Reasoning], context=dict(pr_or_issue=pr_or_issue, labels=labels), agents=[labeler], ) return [r.label_name for r in decision if r.should_apply] </code></pre> <p>In this flow:</p> <ol> <li>We create a Pydantic model to hold the reasoning about each label</li> <li>We create an agent that will use GPT-4o-mini to label the PR or issue</li> <li>We reason about each label to ultimately produce a list of labels that should be applied</li> </ol> <p>The "full" code can be seen <a href="https://github.com/jlowin/ai-labeler/blob/main/src/ai_labeler/ai.py">here</a>.</p> <h3>Reasoning: Show Your Work</h3> <p>The first version of this flow simply asked the agent to generate a list of labels. This worked well in all cases with GPT-4o, but GPT-4o-mini sometimes made mistakes with complex labeling instructions.</p> <p>I experimented with a variety of solutions, including different prompts, multi-stage reasoning, and more, before settling on the approach above, in which we ask the model to explicitly output its reasoning about each label. It's fascinating how well this approach works, permitting GPT-4o-mini to operate near the level of GPT-4o, at a tiny fraction of the cost.</p> <p>(Performance is actually <em>very slightly</em> better in a two-step reasoning approach, but at the cost of a second pass through the LLM, so I've opted for the single-pass version for now)</p> <p>Note that this is <em>not</em> the same as <a href="https://jlowin.dev/blog/does-o1-mean-agents-are-dead">o1-style reasoning</a>, as it does not involve any iterative refinement of the model's understanding of the input. Instead, this approach forces the model to pay more attention to instructions, thereby tipping it into an operating regime that's more likely to produce the right answer.</p> <h2>Configuration</h2> <p>Now we've got an AI workflow that can assign labels to a PR. That still might not be enough to mimic a human maintainer, because we ascribe norms to label application that are based on context, intent, and nuance.</p> <p>For this reason, the AI labeler allows you to annotate each label with natural language instructions that clarify its purpose.</p> <p>The configuration is straightforward:</p> <pre><code>labels: - security-review: # the label name description: "Needs security team review" instructions: | Apply when changes involve: - Authentication or authorization code - Cryptographic operations - Environment variables - Container or deployment config </code></pre> <p>This gives you a way to define what "good first issue" means for your project, or exactly when to flag something as a breaking change or needing tests. The AI will consider your instructions alongside the actual content, leading to remarkably nuanced decisions.</p> <p>For more control, you can provide global instructions and even include additional files for context, like a contribution guide or code of conduct.</p> <pre><code># .github/ai-labeler.yml instructions: | Focus on identifying good first issues and security concerns. labels: - good-first-issue: description: "Perfect for newcomers" instructions: | Apply when the changes are: - Well-scoped and isolated - Well-documented - Don't require deep system knowledge - security-review: description: "Needs security team review" instructions: | Apply when changes touch: - Authentication flows - Environment variables - Network requests context_files: - .github/CODEOWNERS - CONTRIBUTING.md - CODE_OF_CONDUCT.md </code></pre> <h2>No Plan Survives First Contact</h2> <p>I'd never created a GitHub Action before, and I have to admit – it was both more challenging and more rewarding than I expected. The documentation is comprehensive but often cryptic. Environment variables have surprising names. You can't read repository files until you check out the repository (which seems obvious in hindsight, but took me embarrassingly long to figure out). Testing is essentially "push and pray."</p> <p>This led to an important design decision: keep most of the code as normal, testable Python and wrap it with a thin layer of GitHub-specific glue. In retrospect, this separation of concerns was crucial for maintaining my sanity during development and will make it much easier to maintain going forward.</p> <h2>Beyond Labels</h2> <p>What excites me most about this project isn't just its utility (though I do use it on all my repositories now). It's that it represents a perfect example of how AI can augment existing workflows without trying to replace human judgment:</p> <ol> <li>It handles the routine work of initial labeling, but maintainers can always adjust or override its decisions</li> <li>It learns from your repository's context and explicit instructions, adapting to your specific needs</li> <li>It's completely transparent about its reasoning, making it easy to debug and improve</li> <li>It's fast and affordable – you could process 10,000 PRs for less than $5</li> </ol> <p>The structured reasoning approach means we get sophisticated behavior from smaller models – there's no need to step up to GPT-4o or Claude just for intelligent labeling. This keeps it practical for real-world use while still delivering genuinely helpful automation.</p> <p>There's still plenty to explore – for example, distinguishing between issue-only and PR-only labels, or learning from manual corrections over time. Want to help? The project could especially use some "good first issues" to welcome new contributors. You can find <a href="https://github.com/jlowin/ai-labeler">AI Labeler on GitHub</a>, and suggestions are always welcome.</p> <p>But don't worry about picking the right labels for your issues – we've got that covered! 😉</p> An Intuitive Guide to How LLMs Workhttps://jlowin.dev/blog/an-intuitive-guide-to-how-llms-work/https://jlowin.dev/blog/an-intuitive-guide-to-how-llms-work/Chatting by chanceSun, 06 Oct 2024 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro"; import { Image } from 'astro:assets'; import coinToss from './coin-toss.svg'; import diceRoll from './dice-roll.svg'; import heightByAge from './heights-age.svg'; import heightAll from './heights-all.svg'; import heightBasketball from './heights-basketball.svg'; import heightByGender from './heights-gender.svg'; import rouletteSpin from './roulette-spin.svg'; import wordsConditionalCat from './words-conditional-cat.svg'; import words from './words.svg';</p> <blockquote> <p>"LLMs, how do they work?"</p> </blockquote> <p>It may seem like a strange[^magnets] question to ask. After all, large language models (LLMs) have become so ubiquitous so quickly that it's hard to find someone who <em>isn't</em> interacting with one regularly. They form the cornerstone of modern AI, powering everything from consumer-facing chatbots to advanced analysis tools. They can write poetry, answer complex questions, and even build software.</p> <p>[^magnets]: <a href="https://www.youtube.com/watch?v=aVPDGW6Y53s">Magnets, how do they work?</a></p> <p><strong>But how do they work?</strong></p> <p>I believe it's critical to develop a strong intuition for how LLMs operate in order to work with them effectively. Unfortunately, most people are quickly deterred by all the complex math that usually accompanies any such explanation. However, just as you don't need to understand exactly how a car's engine works to be a skilled driver, or know the details of Google's algorithm to craft an effective query, you also don't need to grok transformer models in order to be productive with ChatGPT. What you <em>do</em> need is an understanding of how the system behaves as a whole.</p> <p>It's not as complicated as you might think. See if you can complete this sentence: <code>The cat sat on the _____.</code> Did you think of <code>mat</code>, <code>windowsill</code>, or maybe <code>keyboard</code>? Believe it or not, this post is mostly about designing a system that can do the same. By the end, I hope you'll see how a simple idea like word prediction can scale up to create AI's capable of engaging in complex conversations, answering questions, and even writing code.</p> <p>And we'll only use as much math as you'd be comfortable discussing at a dinner party.[^dinner-party-math]</p> <p>[^dinner-party-math]: Granted, if you regularly discuss math at your dinner parties, this post might not be for you.</p> <p>&lt;Callout color="gray"&gt; This post is based on my talk, &lt;span class="font-bold"&gt;"How to Succeed in AI (Without Really Crying)."&lt;/span&gt; &lt;/Callout&gt;</p> <h2>Probability</h2> <p>To understand how LLMs work, we need to start with probability.</p> <p><strong>I know, you're already bored.</strong> I love statistics, and I'm already bored. But at their core, LLMs are nothing more than fancy probability engines.[^fancy-probability-engines]</p> <p>[^fancy-probability-engines]: with really fancy marketing.</p> <p>Probability is a tool for quantifying randomness and uncertainty. Having a probabilistic nature is the source of an LLM's power... and its unpredictability. It's what makes it possible to generate novel, creative, actionable responses, and also makes LLMs very difficult to train or debug.</p> <p>Whether you're a layperson, practitioner, or researcher, the entire process of working with LLMs is an exercise in forming and manipulating their latent probability distributions into giving you the outputs you want.</p> <p>Therefore, there are three key concepts that, if understood intuitively, will give you a firm grasp on how LLMs work:</p> <ol> <li><strong>Probability Distributions</strong></li> <li><strong>Conditional Probability Distributions</strong></li> <li><strong>Sampling from Probability Distributions</strong></li> </ol> <p>&lt;Callout color="green"&gt; The statistics nerds among you may argue that we should be talking about "joint probability distributions".</p> <p>The statistics nerds among you are welcome to write their own blog posts. &lt;/Callout&gt;</p> <h3>Probability Distributions</h3> <p>Let's dive right in to probability with something that's familiar to most people: flipping a coin.</p> <p>When you flip a fair coin, there's a 50% chance it will land on heads and a 50% chance it will land on tails. This simple scenario is nonetheless a complete example of a probability distribution. Let's break it down:</p> <ul> <li>There are two possible outcomes: heads or tails.</li> <li>Each outcome has an equal likelihood of occurring.</li> <li>The probabilities of all possible outcomes add up to 100%.</li> </ul> <p>We can visualize the distribution of outcomes like this:</p> <p>&lt;figure&gt; &lt;Image src={coinToss} alt="Coin Toss Distribution" class="shadow-none" /&gt; &lt;figcaption&gt;Probability distribution of a fair coin&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>This distribution tells us everything we need to know about a coin toss before it happens. Note that it doesn't tell us what will happen on any particular flip, but rather what to expect over many flips. Another way of saying this is that on any flip, the <em>likelihood</em> of heads is equal to the <em>likelihood</em> of tails. For our purposes today, that likelihood -- or relative chance of an outcome -- is what we're most interested in.</p> <p>But what if there are more than two outcomes?</p> <p>Consider a 6-sided die. Each side has a lower absolute probability of coming up than the side of a coin -- a 16.67% chance, to be precise -- but all of them are equally likely. Therefore, from a probability perspective, a six-sided die is sort of like a scaled-up coin toss: it represents a distribution of equally likely outcomes.</p> <p>&lt;figure&gt; &lt;Image src={diceRoll} alt="Dice Roll Distribution" class="shadow-none" /&gt; &lt;figcaption&gt;Probability distribution of a six-sided die&lt;/figcaption&gt; &lt;/figure&gt; We can push this even further by considering a roulette wheel, which has 38 outcomes, each one just as (un)likely as any other. &lt;figure&gt; &lt;Image src={rouletteSpin} alt="Roulette Spin Distribution" class="shadow-none" /&gt; &lt;figcaption&gt;Probability distribution of a roulette wheel&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>All of the distributions we've discussed so far are called <strong>uniform probability distributions</strong> and if you look at their charts, you can see why: since every outcome is equally likely, their probability distribution is flat.</p> <p>Uniform distributions are an easy and important way to understand the nature of probability, but we all know that LLMs aren't just a giant roulette wheel. We need more powerful tools to understand them.</p> <p>Let's take a step towards complexity by considering probability models that <em>aren't</em> uniformly distributed. One of the most familiar is the <strong>normal distribution</strong>, or bell curve. Consider the following chart of the distribution of adult human heights:</p> <p>&lt;figure&gt; &lt;Image src={heightAll} alt="Height Distribution" class="shadow-none" /&gt; &lt;figcaption&gt;Probability distribution of adult human heights&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>The peak of the curve represents the average height, about 5'5". Heights close to the average are most common, which is why the curve is highest in the middle. As we move away from the average, the likelihood of seeing those heights decreases, which is why the curve tapers off and forms the "bell" shape that gives it its colloquial name.</p> <h3>Conditional Probability Distributions</h3> <blockquote> <p>All models are wrong, but some are useful.</p> <p>-- George Box</p> </blockquote> <p>So is this bell curve a "good" model? It has some nice properties, but it's far from perfect. For example, it suggests that the most likely height for a randomly selected person to have is 5'5". But if you met a 6-year old child who was 5'5", would you think they were completely average? Of course not. And so there's clearly something wrong with our model.</p> <p>Real-world probabilities often depend on additional factors. For example, if we know someone's age, gender, or other demographic information, our assessment of their probable height could change dramatically: the distribution of heights of 6-year old boys is markedly different from middle-aged women. One way of discussing this rich family of related probabilities is that they are <strong>conditional probability distributions</strong>, meaning that they reflect additional information or knowledge versus the base or naive distribution.</p> <p>Here, for example, are the conditional height distributions for adult men and women:</p> <p>&lt;figure&gt; &lt;Image src={heightByGender} alt="Height Distribution by Gender" class="shadow-none" /&gt; &lt;figcaption&gt;Probability distribution of adult male and female heights&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>You can see that there are two distributions, one for each piece of conditional knowledge. If we know someone's gender, we can use the corresponding distribution to make more accurate predictions.</p> <p>Similarly, here are conditional height distributions for 10-year olds and 50-year olds. You can imagine that there is a continuous stream of corresponding distributions for every other age.</p> <p>&lt;figure&gt; &lt;Image src={heightByAge} alt="Height Distribution by Age" class="shadow-none" /&gt; &lt;figcaption&gt;Probability distribution of 10-year-old and 50-year-old heights&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>Conditional distributions allow us to not only model outcomes based on our empirical observations of the world, but to incorporate other findings into those models in a precise way. What's especially interesting is that the conditional factors do not have to have a causal relationship on the observed outcomes; they only need to be correlated with them.</p> <p>To illustrate this important concept, consider the distribution of heights of professional basketball players. The distribution of heights, conditional on the knowledge that someone is a professional basketball player, is quite different from the distribution of heights in the general population.</p> <p>&lt;figure&gt; &lt;Image src={heightBasketball} alt="Height Distribution for Professional Basketball Players" class="shadow-none" /&gt; &lt;figcaption&gt;Probability distribution of heights for professional basketball players, with the population average in gray&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>The peak has shifted significantly to the right, indicating that professional basketball players are, on average, much taller than the general population. But here's the crucial point: being tall doesn't cause someone to become a professional basketball player, nor does being a professional basketball player cause someone to grow taller. There's simply a strong correlation between being tall and being a professional basketball player.</p> <p>This non-causal yet highly informative relationship is key to understanding how LLMs work. These models don't understand causality in the way humans do. Instead, they excel at recognizing and leveraging correlations in data. When an LLM generates text, it's not reasoning about cause and effect; it's making predictions based on patterns and correlations it has observed in its training data.</p> <p>For instance, if an LLM has been trained on a dataset that includes many descriptions of basketball players, it might learn to associate words like "player," "NBA," or "court" with a higher likelihood of words related to tallness. This doesn't mean the model understands why basketball players tend to be tall; it just knows that these concepts frequently co-occur.</p> <h3>Sampling</h3> <p>The last thing we need to understand before we move on to language is <strong>sampling</strong>.</p> <p>Let's go back to our roulette wheel for a moment. When you play roulette, you're using a ball to sample outcomes from the probability distribution of the wheel. Each spin is an independent event that produces an outcome based on the underlying probabilities. To sample digitally, we replace the ball with a random number generator.</p> <p>Sampling is how we turn our probability distributions into actual outcomes. It's the bridge between our model of the world (the distribution) and events in the world (specific outcomes). Given some understanding of the relative likelihoods of different outcomes, we can produce novel outcomes from the distribution that satisfy its rules and constraints.</p> <p>Importantly, sampling allows us to generate outcomes that reflect the overall structure of the distribution, even if we've never seen that exact outcome before. For instance, if we sample heights from our earlier distribution, we might get a height of 5'11" - a specific value that may not have been in our original dataset, but one that fits the pattern we've modeled.</p> <p>This process of sampling is crucial for LLMs. When generating text, these models don't simply choose the most probable word every time. Instead, they sample from their probability distributions, which allows for creativity and variability in their responses. For now, it's important to note that sampling lets you convert a distribution into an outcome, a concept we'll explore further when we dive into how LLMs generate text.</p> <h3>Training</h3> <p>Before we dive into language models, let's briefly touch on what it means to "train" a model for a probability distribution. For our purposes, think of training as the process of tweaking a mathematical formula to make it fit a set of observed outcomes as closely as possible.</p> <p>One of the reasons the normal distribution is so useful is that its model only requires two parameters: the average (mean) height and how much heights typically vary from this average (standard deviation). With these two numbers, we can recreate that familiar bell curve.</p> <p>But what about more intricate distributions, like our conditional probabilities? Well, it gets a bit more complicated, but the core idea is the same: we're trying to create a mathematical model that can accurately represent the distribution we see in our data. For now, just know that it's possible to build these models, even for very complex distributions, and training is the process of solving for their parameters.</p> <h2>Language Models</h2> <p>We've spent considerable time building an intuition for probability distributions, conditional probabilities, and sampling. Now, let's apply these concepts to the core of Large Language Models: modeling language itself.</p> <h3>Distributions of Words</h3> <p>Just as we modeled the distribution of heights in a population, we can model the distribution of words in a language. At first, this might seem strange - words aren't numbers like heights, after all. But remember, probability distributions are simply about the likelihood of different outcomes, and words are just another type of outcome we can measure.</p> <p>Suppose we took a large corpus of text data and made a graph of every word that appeared in it against its normalized frequency of appearance, ordered by that frequency. We'd get something like this:</p> <p>&lt;figure&gt; &lt;Image src={words} alt="Word Frequency Distribution" class="shadow-none" /&gt; &lt;figcaption&gt;Probability distribution of words in the English language&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>This is a probability distribution of words! Just like our height distribution, it shows us the relative likelihood of different outcomes. However, there's a crucial difference: while heights formed a continuous distribution, words are discrete entities. There's no such thing as a word that's halfway between 'cat' and 'dog'. In this sense, our word distribution is more like our roulette wheel: each word is a distinct possibility with its own probability of occurrence.</p> <p>In this distribution, you'll notice:</p> <ol> <li>Common words like <code>is</code>, <code>the</code>, and <code>a</code> are the most likely to appear.</li> <li>Everyday nouns and verbs like <code>street</code>, <code>yellow</code>, and <code>climb</code> occupy the middle ground.</li> <li>There is a long tail of rare or specialized words like <code>oxidize</code> or <code>peripatetic</code>.</li> </ol> <p>However, we can't just sample from this distribution and generate intelligible prose. Iterated draws from this distribution are infinitely more likely to generate the "sentence" <code>a a the a yellow run a the catalyst a is the the street</code> than anything resembling Shakespeare.</p> <h3>Conditional Distributions</h3> <p>Remember how our height predictions improved when we considered additional factors like age or profession? The same principle applies to words, but to an even greater degree. The probability of a word appearing is heavily dependent on the words, syntax, and semantics that come before it. This is where conditional probability becomes crucial in language modeling.</p> <p>Let's go back to the simple example we started this post with:</p> <p><code>The cat sat on the _____</code></p> <p>Given this context, you can make a pretty good guess about what the next word could be:</p> <ol> <li><code>mat</code> is highly probable</li> <li><code>roof</code> is likely</li> <li><code>piano</code> is also possible, though less common</li> <li><code>myrmidon</code> is extremely improbable</li> <li><code>the</code> wouldn't even make sense grammatically</li> </ol> <p>&lt;figure&gt; &lt;Image src={wordsConditionalCat} alt="Conditional Probability of Words Given Context" class="shadow-none" /&gt; &lt;figcaption&gt;Conditional probability of words given context&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>Obviously, this is a very different distribution than the "naive" or unconditional distribution of words. Producing these conditional distributions is the heart of language modeling and the core internal operation of an LLM. A properly trained model can output a distribution like this one for any provided context, or "prompt." As the prompt evolves, so too would the model's assessment of conditional likelihoods.</p> <h3>Generating Text</h3> <p>Now that we understand how individual words can be modeled as probability distributions and even account for context, how do we use this to generate coherent text? This is where sampling comes into play.</p> <p>Sampling from an LLM is similar to when we drew values from a more simple probability distribution, with a catch: we don't want to just pick one word; we want to generate an entire sentence or paragraph! To do this, we sample <em>iteratively</em> from a conditional distribution of words, adding the result of each draw to the context for the next draw.[^inference]</p> <p>[^inference]: The complexity of producing a new probability distribution for every word is why LLM inference is expensive and time-consuming.</p> <p>&lt;Callout color="green"&gt; The LLM nerds among you may notice I haven't mentioned "tokens."</p> <p>In practice, modern LLMs don't work directly with whole words, but rather with tokens that represent groups of characters, including punctuation. There's a variety of reasons for this, including efficiency of encoding and flexibility in handling rare words and misspellings, but the core principles of building and sampling from a distribution remain the same. Anywhere I refer to "words" in this post, you can mentally substitute "tokens" if you prefer. &lt;/Callout&gt;</p> <p>Here is a simple description of the process:</p> <ol> <li>The LLM starts with an initial context (which could be empty or provided by a prompt).</li> <li>Based on this context, it calculates the conditional probability distribution for the next word.</li> <li>It samples a word from this distribution.</li> <li>It adds this word to the context and repeats the process.[^end]</li> </ol> <p>[^end]: At some point it decides to stop, but the details of that are way beyond what we're covering here.</p> <p>Let's illustrate this by continuing our previous example with the cat. The initial context is:</p> <p><code>The cat sat on the _____</code></p> <p>Suppose our model samples <code>roof</code> from the distribution we proposed earlier. Now our context becomes:</p> <p><code>The cat sat on the roof _____</code></p> <p>The model would then calculate a new probability distribution for the next word. This distribution might favor words like <code>and</code>, <code>of</code>, or <code>watching</code>. Let's say it chooses <code>of</code>. The updated context is:</p> <p><code>The cat sat on the roof of _____</code></p> <p>We repeat the process, computing a new conditional distribution for this context. This time it might heavily favor words like <code>the</code>, <code>a</code>, or <code>her</code>. Each choice influences the next, and so on.</p> <p>Here's what it looks like in practice:</p> <p>&lt;iframe class="w-full" height="500" src="https://www.youtube.com/embed/nPN6OHBIcsc?si=1nDijZOXpdpLosT9" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;</p> <p>This is really how LLMs work! An iterative process of sampling and updating the context is fundamentally how LLMs generate text. It's analogous to repeatedly sampling heights from our height distribution, but with each sample influencing the distribution for the next one.</p> <p>However, this approach introduces a significant challenge: compounding errors. Once the model makes a "mistake" or chooses an unlikely word, that choice becomes part of the context for all future words. This can cause the model to veer into increasingly improbable territory, potentially devolving into gibberish after a few words or sentences.</p> <p>Early language models often struggled with this issue. As they started to drift away from highly probable word sequences, they would tip increasingly into a low-probability, high-entropy regime. In a sense, language models are self-reinforcing: the more they favor a certain style, topic, or format, the more likely they are to continue doing so. Conversely, the more they veer into nonsense, the more likely nonsense becomes.</p> <p>This self-reinforcing nature has interesting implications. For instance, once a model outputs a specific idea or format, it can be difficult to tell it to stop doing that.[^bullets] In fact, telling a model <em>NOT</em> to think of something almost always makes it output that very thing. It's the digital equivalent of the classic "don't think of an elephant" thought experiment.</p> <p>[^bullets]: My kingdom for a way to prevent LLMs from resorting to bullet points all the time.</p> <p>The sampling process necessarily introduces an element of randomness, which is crucial for creativity and diversity in the outputs. If the model always chose the most probable word, its outputs would be repetitive and unnatural. The degree of randomness in sampling can be adjusted through a parameter that is often called "temperature":</p> <ul> <li><strong>Low temperature:</strong> The model is more likely to choose high-probability words. This results in more predictable, potentially more coherent, but possibly less creative text.</li> <li><strong>High temperature:</strong> This introduces more randomness, allowing the model to more frequently choose lower-probability words. This can lead to more creative but potentially less coherent outputs.</li> </ul> <p>Modern LLMs have become much better at maintaining coherence over longer stretches of text, thanks to advances in model architecture, training techniques, and the sheer scale of the models. However, the fundamental challenge of compounding errors remains, and it's one of the reasons why LLMs can sometimes produce outputs that start strong but become increasingly nonsensical or off-topic as they continue.</p> <h3>From Chance to Chat</h3> <p>Chat interfaces have become the dominant way for most people to interact with LLMs, capturing the public imagination and showcasing these models' capabilities. But how do we get from generating individual words to engaging in full-fledged conversations? The answer lies in cleverly applying the principles we've discussed so far.</p> <p>Here's how it works:</p> <ol> <li>When you start a chat, your initial message becomes the first piece of context.</li> <li>The model generates a response based on this context, just as we described earlier.</li> <li>For your next message, the model doesn't just look at what you've just said. Instead, it considers everything that's been said so far - your initial message, its first response, and your new message.</li> <li>This process repeats for each turn of the conversation. The context grows longer, incorporating each new message and response.</li> </ol> <p>This approach allows the model to maintain consistency and context throughout a conversation. It can refer back to earlier parts of the chat, answer follow-up questions, and generally behave in a way that feels more like a coherent dialogue than isolated text generation.</p> <p>However, this method also introduces some challenges:</p> <ol> <li> <p><strong>Context Length Limits:</strong> LLMs have a maximum amount of text they can process at once (often referred to as the "context window"). For very long conversations, the earliest parts might get cut off when this limit is reached.</p> </li> <li> <p><strong>Computational Cost:</strong> As the conversation grows, generating each new response requires processing more and more text, which can slow down the model's responses and increase computational costs.</p> </li> <li> <p><strong>Consistency vs. Creativity:</strong> The model might become overly constrained by the conversation history, potentially leading to less diverse or creative responses over time.</p> </li> </ol> <p>Despite these challenges, this simple yet effective approach to chat is what powers the conversational AI interfaces we interact with daily. By treating the entire conversation as a growing context for probabilistic text generation, LLMs can engage in surprisingly coherent and context-aware dialogues.</p> <h3>The Company Words Keep</h3> <p>We've seen how LLMs generate text by iteratively sampling from probability distributions. But where do these distributions come from? How does the model know which words are likely to follow others?</p> <p>Earlier, we touched briefly on <strong>training,</strong> the exercise of discovering the intricate distributions that allow an LLM to predict the next word with such nuance.</p> <p>My colleague <a href="https://www.linkedin.com/in/adam-azzam">Adam</a> (who is, incidentally, the only person still reading this post) has an excellent way of capturing the intuition behind training:</p> <blockquote> <p>"You know a word by the company it keeps."</p> </blockquote> <p>This means that an LLM's understanding of a word is entirely based on how that word appears in relation to other words. Surprisingly, at no time does it learn its definition, etymology, or any other intrinsic property in an explicit way.[^dictionary-training] The goal of training is to build a sophisticated model of these latent relationships to make accurate predictions about which words are likely to appear next.</p> <p>[^dictionary-training]: It's quite likely that a dictionary would be included in a model's training data. However, it would not receive any special attention or processing, though of course the close proximity of a word and its dictionary definition would result in a much stronger relationship between the two.</p> <p>To illustrate this principle in a simple sense, consider the word "bank." In isolation, it could refer to a financial institution or the side of a river. During training, the model might encounter sentences like:</p> <ol> <li>"He deposited money in the <em>bank</em>."</li> <li>"The river overflowed its <em>banks</em> after heavy rain."</li> <li>"The <em>bank</em> approved her loan application."</li> <li>"We had a picnic on the grassy <em>bank</em> by the stream."</li> </ol> <p>How can an LLM learn to distinguish between these meanings? Well, pretty much the same way you do.</p> <p>Over billions of examples, the model builds a nuanced understanding of how the word "bank" relates to other words. It learns that when "bank" appears near words like "money," "deposit," or "loan," it's likely referring to a financial institution. When it's near words like "river," "stream," or "grassy," it's more likely referring to a riverside. This understanding is encoded in the parameters of the model's implicit probability distribution, and those parameters are often referred to as "weights."</p> <p>This "company it keeps" principle is crucial. The model doesn't have explicit definitions or rules about what words mean. Instead, it builds a rich, multidimensional model of how words relate to each other in various contexts.</p> <p>The actual mathematics of how training works is beyond the scope of this post (and, probably, most dinners you'll attend). But conceptually, you can think of it as the model adjusting its internal parameters to better predict the next word in a sequence, given all the words that came before it. It does this over and over, for billions of examples, gradually refining its ability to capture the patterns and relationships in language.</p> <p>What emerges from this process is not a set of rules or definitions, but a vast, interconnected web of probabilities. Given any sequence of words, the model can use this web to calculate the probability distribution of what might come next. This is why models require extraordinary amounts of data, compute, and time to train - they're building an incredibly complex probabilistic model of language itself.</p> <p>Understanding training in this way helps explain some of the quirks and limitations of LLMs:</p> <ol> <li><strong>Correlation, not causation:</strong> LLMs excel at recognizing patterns and correlations in language, but they don't understand causality. This is why they can sometimes produce outputs that seem logical but are factually incorrect.</li> <li><strong>Bias in, bias out:</strong> If the training data contains biases or inaccuracies, these will be reflected in the model's outputs. The model doesn't have a way to fact-check its training data.</li> <li><strong>Hallucination:</strong> When asked about topics it hasn't seen much of in its training data, an LLM might generate plausible-sounding but incorrect information. This is because it's trying to produce probable sequences of words based on limited relevant context.</li> <li><strong>Difficulty with explicit rules:</strong> Because LLMs learn implicitly from patterns rather than explicit rules, they can sometimes struggle with tasks that require strict adherence to specific formats or guidelines.</li> </ol> <p>By understanding LLMs as probability engines trained on vast amounts of text data, we can better appreciate both their capabilities and their limitations. This perspective is crucial for using them effectively and responsibly in real-world applications.</p> <h2>Thinking with Probabilities</h2> <p>Now that we understand LLMs as fancy probability engines, let's explore how this perspective can help us use them more effectively. A lot of common LLM techniques are really just clever ways of nudging these probability distributions. Here are a few examples of ideas and techniques you may have heard of, and how they are actually all just playing with probability:</p> <p><strong>Talk like a pirate:</strong> It's the classic "hello world" of proving your LLM works: getting it to talk like a pirate. By now you should realize that the model doesn't have a separate "pirate mode" - it's just shifting its word probabilities to favor "Arrr" and "matey" over more standard English.</p> <p><strong>Prompt engineering:</strong> In general, all of prompt engineering is all about putting the model in a better "probability regime." When we craft a good prompt, we're not just asking a clear question - we're subtly shaping the likelihood of different kinds of responses. This is why prompts that work well for LLMs might look different from how we'd phrase things to a human or even to a search engine.</p> <p><strong>Chain-of-thought:</strong> One of the most powerful techniques in using LLMs is as simple as asking the model to "think step by step." But why does this work? Remember, our LLMs are making probabilistic leaps from input to output. Sometimes, the leap from question to answer is just too big - the correct answer might be <em>logical</em>, but not <em>probable</em> given the input. By asking for step-by-step reasoning, we're allowing the model to make a series of smaller, more probable jumps. Each step flows more naturally from the last, leading to a better final answer.</p> <p><strong>Fine-tuning:</strong> Sometimes, we want to push our models even further in a particular direction. That's where fine-tuning comes in. Fine-tuning is like giving the model a specialized crash course. We start with a model that has broad knowledge (it's seen tons of text on all sorts of topics), and then we show it a bunch of examples in our area of interest. This nudges the model's entire probability distribution, making it more likely to use certain words or concepts by default.</p> <p><strong>RAG (Retrieval-Augmented Generation):</strong> This powerful technique has a simple but effective idea: before the model generates a response, we fetch some relevant information and add it to the input. This biases the model's output probabilities towards using this specific, relevant information. It's a bit like giving the model a cheat sheet for the particular question you're asking.</p> <p><strong>Translation:</strong> Using statistics and correlative probabilities to model the relationship between words in different languages is not new; in fact, about a decade ago it provided a revolutionary step forward in high-quality machine translation. As considerably more powerful general-purpose models, LLMs inherit this ability to model and predict words across languages. You now know enough to think of this probabilistically: given a sentence and an instruction to translate it, a properly-trained model should determine that the most probable outcome is the translation.</p> <p><strong>Tipping your LLM:</strong> Consider the trick of saying "I'll tip you $20" to an AI assistant. This doesn't work because the model is actually expecting payment. Instead, it's putting the model into a state where it's more likely to produce high-effort, high-quality responses. It's learned that contexts involving rewards often come with expectations of better performance.</p> <p><strong>ReAct agents:</strong> This idea of guiding the model's reasoning process is also behind more complex systems like ReAct agents. These are setups where we give the model a specific format to follow, usually involving steps like "Think, Act, Observe." By being precise about what we expect, we make it more likely for the model to use tools effectively or to check its own work.</p> <p><strong>Code generation:</strong> When it comes to generating specific types of content, like code, we can push this idea of biasing probabilities even further. When we tell a model to "write Python code," we're not activating some separate coding module. Instead, we're shifting the model into a state where it's much more likely to produce text that looks like Python - lots of indentation, specific keywords, that sort of thing.</p> <p><strong>Structured output generation:</strong> For highly structured outputs like JSON, some systems even artificially limit which tokens (chunks of text) the model is allowed to produce. This ensures the output follows the correct format, essentially forcing the model to color within the lines we've drawn.</p> <p><strong>Recitation:</strong> When the public first became aware of LLMs, there was a sustained and false belief that the models somehow maintained a copy of the entire internet, which was remixed or regurgitated on demand. Perhaps this was easier for some people to believe than models being capable of synthesizing novel outputs. The most common evidence for this belief was that models could perfectly recite known documents, like the first three paragraphs of Alice in Wonderland. By now, I hope you appreciate that for a sufficiently trained model, this is neither surprising nor particularly impressive. After all, the most probable response to "What are the first three paragraphs of Alice in Wonderland?" is, of course, the first three paragraphs of Alice in Wonderland.</p> <p>All of these techniques, from simple prompt tweaks to complex system designs, are really just ways of playing with probabilities. We're constantly asking ourselves: how can we make the output we want more likely? How can we guide the model towards better reasoning, more accurate information, or more useful formats?</p> <h2>Coda</h2> <p>We've journeyed from coin flips to complex language models, all through the lens of probability. I hope that you have developed a solid intuition for how LLMs actually work:</p> <ul> <li>They're built on sophisticated probability distributions of language.</li> <li>They generate text by iteratively sampling from these distributions.</li> <li>Their "knowledge" is really just a vast web of word relationships and correlations.</li> </ul> <p>Understanding LLMs as probability engines rather than knowledge databases is crucial for using them effectively and responsibly. It helps us set realistic expectations, interpret their outputs appropriately, and design better ways of leveraging their capabilities.</p> <p>As we continue to develop and refine these models, keeping this probabilistic perspective in mind will be key. It reminds us that while LLMs are incredibly powerful tools that can revolutionize how we interact with information and solve problems, they're fundamentally playing a very advanced game of "what word comes next?"</p> <p>They're not magic, they're not sentient, and they're definitely not going to rise up and kill us all.</p> <p>Probably.</p> Beyond Reasoning: Anthropic's Agenthttps://jlowin.dev/blog/beyond-reasoning-anthropic-agent/https://jlowin.dev/blog/beyond-reasoning-anthropic-agent/If you give a computer a computer...Tue, 22 Oct 2024 00:00:00 GMT<p>When o1 was released, I <a href="/blog/does-o1-mean-agents-are-dead">wrote</a> that internal reasoning - even iterative reasoning - didn't represent agentic behavior. I defined an agent as requiring iterative interactions with the external world: perceiving the environment, taking actions, observing outcomes, and adjusting accordingly. With Anthropic's release of their new "<a href="https://www.anthropic.com/news/3-5-models-and-computer-use">computer use</a>" feature, we're seeing exactly this kind of genuine agency in action.</p> <h2>Why This Is Different</h2> <p>The fundamental difference isn't in the complexity of the tasks or the sophistication of the AI - it's in the presence of a real-world feedback loop. When an AI reasons internally, it can refine its thinking and generate better answers, but it's still operating in a closed system of its own knowledge and patterns. In contrast, Anthropic's agent actually interacts with computer systems, observes the results of its actions, and adjusts its behavior based on what really happened, not what it predicted would happen.</p> <p>This is what makes it a true agent. When operating a computer:</p> <ol> <li>It perceives the environment through screenshots, understanding complex visual interfaces</li> <li>It translates high-level goals into specific actions (mouse movements, keyboard inputs)</li> <li>It observes the results of those actions through new screenshots</li> <li>It adjusts its strategy based on what it learns from those results</li> </ol> <h2>Governing Real-World Agency</h2> <p>This shift from reasoning to real-world agency demands entirely new frameworks for defining and controlling AI behavior. With pure reasoning systems, we could focus on input filtering and output validation. But with true agents that learn and adapt through interaction, we need systems that can:</p> <ol> <li>Define acceptable boundaries of exploration - how do we let agents learn from their mistakes without causing harm?</li> <li>Monitor behavioral patterns, not just outputs - when an agent develops a new strategy through real-world interaction, how do we evaluate if it's safe and appropriate?</li> <li>Establish clear lines of responsibility - when an agent makes decisions based on its own observations and learning, who is accountable for the outcomes?</li> </ol> <p>The traditional approach of treating AI systems as deterministic tools breaks down here. We need frameworks that can handle emergent behavior while maintaining meaningful human oversight. This isn't just about safety guardrails - it's about developing new ways to specify goals and expectations for systems that can discover novel approaches to achieving them.</p> <h2>Looking Forward</h2> <p>The development of true AI agents is a watershed moment that demands new thinking about AI governance. We need frameworks that can balance the benefits of autonomous learning and adaptation with the need for predictability and control. This isn't just a technical challenge - it's a fundamental shift in how we think about AI systems and their relationship to the world they operate in.</p> <p>The question isn't whether we <em>should</em> build AI agents - the horse is wayyyy out of the barn. The question is how we develop the systems of governance and control that will let us harness their capabilities safely and effectively. This is the next great challenge in AI development.</p> Introducing Colinhttps://jlowin.dev/blog/colin/https://jlowin.dev/blog/colin/A context engine that keeps agent skills fresh.Fri, 23 Jan 2026 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>&lt;Callout color="blue"&gt; <strong>tl;dr</strong> Colin is an experimental context engine that can load dynamic information into agent skills and keep them fresh. Give it a star on <a href="https://github.com/PrefectHQ/colin">GitHub</a>! &lt;/Callout&gt;</p> <p>Context goes stale.</p> <p>This is an increasingly serious problem for anyone building with agents, and it manifests in a few different ways. Stale context can mean:</p> <ul> <li><strong>The information is out of date</strong>: a skill that was accurate when written but hasn't been touched since.</li> <li><strong>The information is unavailable</strong>: a conversation was compacted and details didn't survive the summary.</li> <li><strong>The information is siloed</strong>: it exists, but in a different conversation, a different chat window, or a different day.</li> </ul> <p>There are two major standards for delivering context to agents today: <a href="https://modelcontextprotocol.io/">MCP</a> and <a href="https://agentskills.io/">agent skills</a>. They occupy opposite ends of a spectrum, and neither has a particularly good solution to this problem.</p> <p><strong>Skills</strong> are optimized for <em>passive access to static information</em>. Drop a markdown file in a folder and the agent discovers it when relevant. Skills are lightweight, progressively disclosed, and always available.</p> <p><strong>And that's a problem.</strong></p> <p>Skills are markdown, so updating them means editing files by hand. Most of us don't, and so our agents' skills decay, becoming less relevant over time.</p> <p><strong>MCP</strong> is optimized for <em>active retrieval of dynamic information</em>. The agent fetches what it needs on demand, so the information is always current.</p> <p><strong>And that's a problem.</strong></p> <p>MCP requires <strong>conversational boilerplate</strong> because every conversation starts from scratch, so the agent has to figure out what it needs, invoke tools, load data, and accumulate knowledge. This is a lot of cycles and tokens spent setting up context that the agent already had yesterday.</p> <p>Wouldn't it be nice to combine the dynamicism of MCP—open tickets, customer requests, upcoming meetings, recent PRs—with the passive availability of skills? Then our agents would have way to consistently access a flow of constantly changing information. No copying and pasting. No waiting for tools to load. No hoping the agent sets up context the same way it did yesterday.</p> <p>We need a way to combine the best of both worlds. And to <a href="https://www.youtube.com/watch?v=w5kBDt6G_h4&amp;t=35s">quote</a> the late Tom Lehrer: I have a modest example here.</p> <hr /> <p><a href="https://github.com/PrefectHQ/colin">Colin</a> is an experimental context engine that keeps agent skills fresh. It works by treating skills as software.</p> <p>Colin combines two major capabilities:</p> <p><strong>A powerful templating engine</strong> that can load information from dynamic sources and (optionally) process it with LLMs. Templates can reference other files, GitHub files and PRs, Notion pages, Linear issues, any MCP server, and more. The templating language is Jinja, extended with providers for dynamic content and filters for LLM processing in order to summarize, classify, and extract information to your editorial specifications.</p> <p><strong>A dependency resolution system</strong> that tracks every reference to dynamic content and forms a resolution graph. When you compile a template, Colin traces that graph, evaluates all the references, and only updates the parts that have actually changed. Staleness can be content-based (the source changed), time-based (an hour passed), or both. Colin caches the rest (including LLM calls) in order to incrementally materialize your context.</p> <p>Together, these let you write context that ranges from completely static to fully dynamic and LLM-processed, and use Colin to keep it up to date.</p> <p>You can use Colin's output however you want: it's just markdown. But to me, compiling agent skills is the obvious use case because the world has settled on them as the standard way to provide file-based context to agents. Therefore, Colin has first-class support for writing output directly to your skills folder. But the engine is equally happy to produce documentation, reports, configuration, and anything else you need to keep up to date.</p> <p>Here's what a Colin template looks like:</p> <pre><code>--- name: team-status description: Current state of platform team work colin: cache: expires: 1d --- # Team Status ## In Progress {% for issue in colin.linear.issues(team='Platform', state='In Progress') %} - {{ issue.identifier }}: {{ issue.title }} ({{ issue.assignee }}) {% endfor %} ## Summary {{ ref('team/weekly-notes.md').content | llm_extract('key blockers and priorities') }} </code></pre> <p>Once compiled, Colin knows how to keep this skill up to date. The <code>ref()</code> creates a dependency on <code>weekly-notes.md</code>. The Linear call creates a dependency on those issues. The <code>cache</code> directive enforces time-based staleness. Colin watches all of it, and recompiles when something changes.</p> <h2>Try It</h2> <p>We just open-sourced Colin. It's experimental, it's going to grow, and I hope you'll have fun with it. Please give it a star if you think it'll be useful!</p> <ul> <li><strong>Get the code:</strong> <a href="https://github.com/PrefectHQ/colin">github.com/PrefectHQ/colin</a></li> <li><strong>Read the docs:</strong> <a href="https://colin.prefect.io">colin.prefect.io</a></li> <li><strong>Try it out:</strong> <code>pip install colin-py</code></li> </ul> <p>One fun thing: Colin's <a href="https://colin.prefect.io/docs/getting-started/quickstart">quickstart</a> actually compiles <em>itself</em> into a live-updating skill, so any time we update the docs, your agent automatically learns the new features. Ambitious? Yes. But easy? Also yes!</p> <p>Happy context engineering!</p> 10 Years of Real Good Coffeehttps://jlowin.dev/blog/ten-years-of-real-good-coffee/https://jlowin.dev/blog/ten-years-of-real-good-coffee/Just brew it.Fri, 20 Sep 2024 00:00:00 GMT<p>import { Image } from 'astro:assets'; import banner from './banner.png'; import ivyCity from './ivy-city.png'; import lowinBlend from './lowin-blend.png'; import openingDay from './opening-day.png'; import sampleRoaster from './sample-roaster.png'; import sanitizer from './sanitizer.png'; import shelves from './shelves.png';</p> <p>Today is <a href="https://www.compasscoffee.com/">Compass Coffee's</a> 10th birthday.</p> <p>To most people, Compass is a perpetually buzzing, quickly growing chain of cafes in Washington, DC. But as Compass's <strong>"Global Ambassador,"</strong> a title earned through years of cheerful, unpaid labor, I've been fortunate enough to get swept up in an entrepreneurial whirlwind that has been nothing short of extraordinary.</p> <p>The Compass story doesn't unfold for me in a neat, chronological order. Instead, it comes in a dizzying flood of memories, each one a testament to the chaos of helping close friends build something from the ground up. One moment, I'm in a tiny basement kitchen, being the guinea pig for the founders' first-ever latte, made with beans produced by their small, sample roasting machine. The next, I'm holding a ladder at 2 AM while we fix the front door lock of a new cafe.</p> <p>&lt;figure&gt; &lt;Image src={sampleRoaster} alt="The first sample roaster." /&gt; &lt;figcaption&gt;Michael with the original sample roaster, 2013&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>There's no rhyme or reason to how I found myself doing what needed to be done. Armed with a titleless business card, I became a chameleon, morphing into whatever Compass needed at any given moment. IT guy troubleshooting internet installation? <em>Check.</em> Impromptu CFO to negotiate a lease? <em>You bet.</em> Fill-in baker producing countless, fluffy biscuits? <em>Somehow, also yes.</em></p> <p>The lines between my life and Compass blurred. I'd wake up in a daze, realizing that once again I'd been "Tom Sawyer-ed" into yet another Compass adventure.[^1] One day we'd be in New York, meeting (and usually rejecting) potential investors. The next we'd be in the Nevada desert, getting a multi-day certification in coffee chemistry. We spent the next year working on the empirical data problem of designing a consistent roast profile that tasted the same in the winter as it did in DC's humid summer.</p> <p>&lt;figure&gt; &lt;Image src={openingDay} alt="Opening day in Shaw, 2014." /&gt; &lt;figcaption&gt;Opening day in Shaw, 2014&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>Most mornings, I'd commute across town to the Shaw cafe, making it my makeshift office before heading home to my "real" job. If I was lucky, I'd get a chance to take orders for an hour. Even my two-year-old son got in on the act, offering customers "normal" or "spicy" water--which, to this day, is how the Lowin and Haft households refer to sparkling water.</p> <p>But it wasn't just about the tasks or the roles. It was about being part of something bigger, something growing and evolving at breakneck speed. I felt a surge of pride with each new cafe opening; today there are 20. I watched as Compass products appeared on Whole Foods shelves, launching a wholesale business that today includes many of DC's most popular restaurants. I remember Michael's late-night musings about the possibility of opening a drive-thru, tempered by his fears that it would dilute the personal touch that made Compass special.[^2]</p> <p>Because what truly sets Compass apart isn't just the quality of its coffee—it's the quality of its connections. Michael always emphasized that <strong>a barista's true mission wasn't to serve coffee, but to create regulars</strong>. This philosophy—elevating a transaction into a relationship—is the cornerstone of every great business, whether you're brewing lattes or building software.[^3]</p> <p>&lt;Image src={shelves} alt="Compass shelves" /&gt; The journey wasn't always smooth. When the pandemic hit, I felt the weight of every word in Michael's memo about what it would take for the company to survive. With every cafe forcibly closed, Operation Phoenix was launched by a small, dedicated team and a contract to manufacture hand sanitizer for the city. It's well known that Compass was founded by two Marine officers who brought their work ethic and leadership principles to the coffee business; few times have I seen that matter more than successfuly steering the company through that period.</p> <p>&lt;figure&gt; &lt;Image src={sanitizer} alt="Hand sanitizer" /&gt; &lt;figcaption&gt;Making hand sanitizer, 2020&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>Through it all, Compass gave me more than just a front-row seat to entrepreneurship. It gave me a crash course in the grit, passion, and sheer will it takes to turn a dream into reality. From that first tiny sample roaster to the soaring glass conveyers of the 50,000-square foot Ivy City roastery, I've been there, sometimes helping, sometimes cheering, but always in awe of the journey.</p> <p>&lt;figure&gt; &lt;Image src={ivyCity} alt="Ivy City Roastery" /&gt; &lt;figcaption&gt;The Ivy City Roastery, 2024&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>The intersection between Compass and Prefect deepened too. At Prefect's first conference back in 2018, we had no product to show—just 9 gallons of Compass Coffee and a banner promising "we have free coffee and you don't even have to talk to us." That coffee-stained banner, part of which hangs behind my desk, is a tangible reminder of how Compass's story and mine have intertwined. In 2020, we began sending regular shipments of custom Compass tins to all of our employees, investors, and customers; nothing we've ever done has gotten such a positive reaction.</p> <p>&lt;figure&gt; &lt;Image src={banner} alt="Prefect &amp; Compass." /&gt; &lt;figcaption&gt;The 2018 Prefect banner and custom Compass tins&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>Perhaps the most poignant symbol of this journey is a small, brown paper bag with a handwritten label: <em>"Lowin Blend August 2013"</em>. It's a relic I discovered moving a few years ago, containing the beans from that very first basement roast. Today it sits in the Compass offices as the earliest custom "tin" still in existence. It is a time capsule of dreams, friendship, and what it means to build something that lasts.</p> <p>&lt;figure&gt; &lt;Image src={lowinBlend} alt="Lowin Blend - August 2013" /&gt; &lt;figcaption&gt;The original Lowin Blend and some of its descendents&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>Congratulations, Michael and the entire Compass team!</p> <p>Here's to Real Good Coffee, and a latte great memories.</p> <p>[^1]: Michael has a unique ability to "Tom Sawyer" me into doing work by convincing me it would be fun. It's been more than a decade and I still fall for it.</p> <p>[^2]: Compass would finally open its first drive-thru location in 2022.</p> <p>[^3]: Danny Meyer discusses this idea on an <a href="https://joincolossus.com/episode/meyer-the-power-of-hospitality/">episode</a> of Invest Like the Best.</p> The Covid "Thank You" Surgehttps://jlowin.dev/blog/covid-thank-you/https://jlowin.dev/blog/covid-thank-you/A viral trendSat, 05 Oct 2024 00:00:00 GMT<p>Covid caused many statistical anomalies, but my favorite is the spike in searches for "thank you."</p> <p>Talk about a viral trend.</p> <p><img src="./thank-you.jpg" alt="" /></p> Bluesky-Powered Blog Commentshttps://jlowin.dev/blog/bluesky-comments/https://jlowin.dev/blog/bluesky-comments/Threading the needleMon, 25 Nov 2024 00:00:00 GMT<p>I'm a big fan of Bluesky, and I just added comments to this blog by leveraging its open protocol.</p> <p>The core idea was inspired by Emily Liu's <a href="https://emilyliu.me/blog/comments">excellent post</a> and Jade Garafola's <a href="https://blog.jade0x.com/post/adding-bluesky-comments-to-your-astro-blog/">Astro adaptation</a>, and is delightfully simple: instead of maintaining a separate comment system, why not use the conversations already happening on Bluesky?</p> <p>This is particularly compelling for static sites like this one. Static sites are wonderful - they're fast, secure, and incredibly simple to maintain. But they have one major limitation: they're, well, <em>static</em>. Adding dynamic features like comments traditionally meant either using a heavy third-party service or maintaining a separate database and API (defeating the point of being static).</p> <p>Bluesky offers an intriguingly lightweight alternative. Each blog post corresponds to a Bluesky thread, and comments are just replies to that thread. The heart of the implementation is remarkably simple - it's just a single API call:</p> <pre><code>// Fetch thread replies from Bluesky's API const endpoint = `https://api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=${uri}`; const response = await fetch(endpoint); const data = await response.json(); const comments = data.thread?.replies || []; </code></pre> <p>Everything else - the layout, styling, error handling - become implementation details. When someone visits your blog, the page fetches replies directly from Bluesky's API. There's no database to manage, no auth system to build, no spam to filter - Bluesky handles all of that.</p> <p>What I love about this approach is how it solves multiple problems at once:</p> <ol> <li><strong>Zero maintenance</strong> - The entire system is serverless and requires no ongoing administration</li> <li><strong>Built-in moderation</strong> - Bluesky's native moderation tools (blocking, muting) automatically apply to your comments</li> <li><strong>Genuine conversations</strong> - Comments aren't siloed on your blog; they're part of the open social network</li> <li><strong>Full portability</strong> - Since comments are just Bluesky posts, they're accessible through the API and can move with you</li> <li><strong>Progressive enhancement</strong> - The blog remains fully static and functional even if Bluesky is down</li> </ol> <p>This perfectly exemplifies the power of open protocols. Instead of building yet another commenting system from scratch, we can compose existing infrastructure in creative ways. The AT Protocol provides the social graph, authentication, moderation, and storage - we just need to pipe the data to where people want to see it.</p> <p>Want to see it in action? This post is connected to [this Bluesky thread]. Reply there and watch your comment appear below! And if you implement this on your own blog, let me know - I'd love to see how others adapt and improve upon this pattern.</p> <p>I expect we'll see many more examples of this approach as the AT Protocol ecosystem matures. The web is more interesting when it's interconnected, and open protocols are how we get there.</p> The Curse of ChatGPThttps://jlowin.dev/blog/the-curse-of-chatgpt/https://jlowin.dev/blog/the-curse-of-chatgpt/Missing the LLM forest for the chatbot trees.Wed, 18 Sep 2024 00:00:00 GMT<p>I know you've heard it:</p> <blockquote> <p>"Why can't ChatGPT do this?"</p> </blockquote> <p>It's the 2024 equivalent of "Why won't Google do this?" – an absurd query that has long been the shallowest VC litmus test for early-stage ideas.[^1] But this updated question is asked more often, and more seriously, because ChatGPT has become the default benchmark for what's possible in AI.</p> <p>Part of the phenomenon is familiar, if unusual: the near-total conflation of a new technology with a single product implementation. A handful of contemporary examples exist: Google, Photoshop, the iPad, the Walkman, Velcro. But there's something very different about ChatGPT: it is the first time that I can think of where <strong>the underlying technology is evolving faster than the applications built on top of it</strong>.</p> <p>In the AI space, it's the core models doing the disrupting, not the startups. Each new release leapfrogs forward, threatening to obsolete entire application layers. AI startups must not only keep pace with competitors but also adapt to an environment where foundational breakthroughs constantly redefine product strategies.</p> <p>ChatGPT's potency lies in it's dual nature. It is simultaneously:</p> <ol> <li>a showcase for the state-of-the-art frontier of LLM capabilities</li> <li>a very narrow UX for single-threaded chat</li> </ol> <p>That's an extremely potent combination, and as a result, ChatGPT has become the de facto standard for what an "LLM interface" should be. <strong>And that's a problem, because chat is a truly terrible interface for most AI applications.</strong> Real-world software applications have requirements that don't fit well into a chat interface, even one delivered as an API. They need efficiency, precision, automation, integration, scalability, observability, and reproducibility. I don't want to chat with my {docs, code, toaster, etc.} -- I want to <em>do things</em> with them.</p> <p>But the trouble with this ruthlessly effective combination of technology and interface is that it's created an unusually rigid definition of what "AI" is, and it's hurting innovation. Introducing an effective AI-powered product that <em>isn't</em> chat-based means solving two problems: proving the AI works, and justifying the unfamiliar interface.</p> <p>We need to shift our perspective. LLMs are fundamentally a technology for transforming tokens, not a product in themselves. Instead of inviting users to chat, we should focus on how core LLM operations[^2] can deliver value, then build features around those capabilities. To compete with the ChatGPT standard, prioritize the user experience (or developer experience), not the raw LLM capabilities.</p> <blockquote> <p>Arguably, the most impactful consequence of ChatGPT's success is that LLMs have become a commodity, and the real battleground is the experience of using them.</p> </blockquote> <p>The path forward lies in treating AI like other powerful technologies – as tools to be integrated, not products to be imitated. We don't trumpet that we chose DuckDB (for example); we simply use it create better software. Similarly, AI should enhance our applications without being their focal point.</p> <p>To truly innovate in this space, we must look beyond ChatGPT and see the forest for the trees. By treating AI as the transformative technology it is, rather than a product to be copied, we can unlock its full potential and create applications that genuinely push boundaries.</p> <p>The next time you hear "Why can't ChatGPT do this?" reframe it:</p> <blockquote> <p>"I see how ChatGPT might demo this. How are you going to deliver it to users?"</p> </blockquote> <p>[^1]: VC readers: I'm not talking about you. I'm talking about those <em>other</em> VCs. [^2]: Summarization, extraction, generation, and classification. More on this in a future post.</p> Introducing FastMCP 🚀https://jlowin.dev/blog/introducing-fastmcp/https://jlowin.dev/blog/introducing-fastmcp/Because life's too short for boilerplateSun, 01 Dec 2024 00:00:00 GMT<p>Last week, Anthropic <a href="https://www.anthropic.com/news/model-context-protocol">introduced</a> the Model Context Protocol (MCP), a new standard for connecting AI models to data and tools. Think of it as a universal remote for the internet - a way for AI to safely interact with databases, files, APIs, and internal tools through a common interface.</p> <p>The protocol is powerful but implementing it involves a lot of boilerplate - server setup, protocol handlers, content types, error management. You might spend more time writing infrastructure code than building things the AI can actually use.</p> <p>That's why I built <a href="https://github.com/jlowin/fastmcp">FastMCP</a>. I wanted building an MCP server to feel as natural as writing a Python function. Here's how it works:</p> <pre><code>from fastmcp import FastMCP mcp = FastMCP("File Server") @mcp.resource("file://{path}") def read_file(path: str) -&gt; str: """Read a file from disk""" with open(path) as f: return f.read() @mcp.tool() def append_to_file(path: str, content: str) -&gt; None: """Append content to a file""" with open(path, 'a') as f: f.write(content) </code></pre> <p>That's it. No protocol details, no server lifecycle, no content types - just Python functions that define what your AI can do.</p> <h2>Pure Logic, No Boilerplate</h2> <p>Since FastMCP is built around standard Python functions, you can integrate any kind of functionality. Need database access? File operations? API calls? Just write the function that does it:</p> <pre><code>@mcp.tool() def search_docs(query: str) -&gt; list[str]: """Search documentation""" results = elastic.search(index="docs", q=query) return [hit["_source"]["content"] for hit in results["hits"]["hits"]] @mcp.resource("profile://{user_id}") def get_profile(user_id: str) -&gt; str: """Get user profile""" return get_user_profile(user_id) </code></pre> <p>Each decorator tells FastMCP how to integrate your function:</p> <ul> <li><strong>Resources</strong> provide data (like schemas or file contents). Think of these like GET endpoints for populating context.</li> <li><strong>Tools</strong> perform actions (like searches or updates). Think of these like POST endpoints for performing actions.</li> <li><strong>Prompts</strong> define templates for common interactions.</li> </ul> <p>Everything is just Python - FastMCP handles the protocol machinery.</p> <h2>Why This Matters</h2> <p>Right now, everyone building AI applications has to write their own integrations from scratch. It's like if every website had to implement its own version of HTTP. MCP provides a standard way for AI models to interact with data and tools, and FastMCP makes it dead simple to implement that standard.</p> <p>Instead of building custom agents or copying data into prompts, you can publish a clean interface that any AI model can use. Want to make your company's data searchable? Create an MCP server. Want to let AI models use your internal tools? MCP server. Want to permit AI's to safely access your product? You get the idea.</p> <p>Think of FastMCP as FastAPI for AI-native APIs - a microframework for building functionality over a standard protocol. I built the initial version in about 24 hours of excited hacking after MCP was announced, but it's quickly grown beyond that. The community has already contributed excellent examples, bug fixes, and feature ideas. If you're interested in making AI integration simpler and more standardized, we'd love to have you join us!</p> <p>Come check out the examples, open an issue, or submit a PR - let's make AI integration feel natural for everyone.</p> <p>Give FastMCP a star <a href="https://github.com/jlowin/fastmcp">on GitHub</a>, and happy engineering!</p> Reflecting on FastMCP at 10k stars 🌟https://jlowin.dev/blog/fastmcp-2-10k-stars/https://jlowin.dev/blog/fastmcp-2-10k-stars/Let's git goingFri, 16 May 2025 00:00:00 GMT<p>import { Image } from 'astro:assets'; import history_img from './history.png';</p> <p>It took <a href="https://github.com/PrefectHQ/prefect">Prefect</a> almost 4 years to reach 10,000 GitHub stars.</p> <p>It took <a href="https://github.com/jlowin/fastmcp">FastMCP</a> about 6 weeks.[^1]</p> <p>&lt;Image src={history_img} alt="FastMCP History" class="mx-auto" width="600"/&gt;</p> <p>FastMCP is the fastest-growing open-source project I've ever been a part of. At this point, factoring in FastMCP 1.0's inclusion in the official MCP SDK, it's at the heart of almost every Python MCP server.</p> <p>But whereas Prefect's growth came from providing an excellent developer experience in a domain that users traditionally hate, FastMCP's growth has come from providing an excellent developer experience in a domain that's exploding in popularity. It's hard not to be reminded of <a href="https://x.com/patrick_oshag">Patrick O'Shaughnessy's</a> clear instruction: "Just build something people want."</p> <p>But like Prefect, I didn't build FastMCP because <em>people</em> wanted it. I built it because <em>I</em> wanted it.</p> <p>When it was introduced last year, the Model Context Protocol (MCP) seemed like a really interesting idea... but it was beyond cumbersome to interact with. FastMCP 1.0 aimed to simplify, dareisay make pleasant, the experience of building MCP servers. It was so effective that Anthropic adopted it as the reference implementation for the official MCP SDK.</p> <p>As MCP has gotten swept up in hype in the last month, the rough edges around the young protocol have become even more apparent. The core team is trying to rapidly satisfy the community demand for <strong>MORE</strong> and the naysayer demand for <strong>WHY</strong>. There's confusion about what needs to be implemented rather than adopted, for example with auth. There are questions about transports, like whether to use SSE or "streamable" HTTP; both of which seem like overkill for the most common use cases. And there is, of course, the overarching objection: What was wrong with regular old APIs?</p> <p>MCP is the poster child for tech that's <em>useful</em> beating tech that's <em>perfect</em>. I have my own opinions on the protocol (tldr: I like standards, I'm excited about the second-order features that go well beyond request/response, I really wish the reference SDK wasn't being built by committee) but I'm proud that FastMCP played such an integral role in achieving that <strong>approachable utility</strong>.</p> <p>So what's next?</p> <p>Historically, FastMCP (1.0) focused on merely providing a pleasant DX over the low-level SDK. With <strong>2.0</strong>, we're providing a full ecosystem of servers, clients, and tooling. It is still hard to stand up a fully authenticated remote MCP server... and it's even harder to do anything with it. FastMCP 2.0's headline features like server proxying and composition only scratch the surface of how we can build an integrated, LLM-accessible contextual landscape.</p> <p>See you at 20k.</p> <p>[^1]: Well, 6 weeks from re-launching the project as FastMCP 2.0; 6 months from the first 1.0 commit.</p> FastMCP 2.11: AuthKit + New OpenAPI Parserhttps://jlowin.dev/blog/fastmcp-2-11-auth-openapi/https://jlowin.dev/blog/fastmcp-2-11-auth-openapi/Auth to a good startFri, 01 Aug 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>Here's a dirty secret about MCP servers: almost nobody implements authentication. Not because they don't want to—because it's genuinely impossible.</p> <p>Look at the "simple" auth examples in the official MCP repository. They're 300+ lines of OAuth boilerplate, token validation, error handling, and security edge cases that would make a seasoned backend engineer weep. The message is clear: if you want auth, you're on your own. Good luck figuring out PKCE flows, refresh token rotation, and session management while you're trying to build your actual business logic.</p> <p><strong>We worked with our partners at WorkOS to change that.</strong></p> <p>FastMCP 2.11 introduces our first major step into enterprise-ready authentication, reducing those 300 lines to essentially a one-liner. It also includes a completely rewritten OpenAPI parser, delivering on the promise of using REST APIs as a thoughtful starting point for MCP servers.</p> <p>&lt;Callout color="blue"&gt; <a href="https://github.com/jlowin/fastmcp">Give us a star on GitHub</a> or check out the updated docs at <a href="https://gofastmcp.com">gofastmcp.com</a>. &lt;/Callout&gt;</p> <h2>Authentication: The Problem Nobody Talks About</h2> <p>I've spent months watching developers in the MCP community hit the same wall. They build a brilliant MCP server with amazing tools and resources. They're ready to deploy to production. Then they ask: "How do I add authentication?"</p> <p>Silence.</p> <p>Production-grade auth is a specialized domain. OAuth 2.1, JWT validation, PKCE—these are table stakes for handling user data, and getting them wrong can sink your application. Developers have been forced to choose: ship without auth (unacceptable) or spend weeks becoming security experts (a distraction, to say the least).</p> <h2>Enter WorkOS: DCR-Compliant Auth as a Service</h2> <p>Our partnership with WorkOS emerged from a simple observation: the best auth is auth you don't have to implement yourself. WorkOS AuthKit provides enterprise-grade authentication that is fully compliant with the MCP specification's requirement for Dynamic Client Registration (DCR).</p> <p>Working together, we've built authentication directly into FastMCP's core. The result is plug-and-play auth that doesn't require you to become a security expert overnight:</p> <pre><code>from fastmcp import FastMCP from fastmcp.server.auth.providers.workos import AuthKitProvider mcp = FastMCP( name="SecureServer", auth=AuthKitProvider( authkit_domain="your_authkit_domain", base_url="http://localhost:8000", # your server's URL ) ) @mcp.tool def sensitive_operation(): """This tool now requires authentication.""" return "Only authenticated users can call this" </code></pre> <p>Behind this simple interface, FastMCP handles token validation, user session management, and all the OAuth complexity, cleanly rejecting unauthorized requests before they reach your business logic.</p> <p>For teams that need custom authentication flows, we've also introduced the <code>TokenVerifier</code> protocol—a clean interface for implementing your own auth logic while still leveraging FastMCP's built-in security patterns.</p> <h2>OpenAPI: The Redemption Arc</h2> <p>A few months ago, I wrote a post titled <a href="/blog/stop-converting-rest-apis-to-mcp">"Stop Converting Your REST APIs to MCP"</a>. I stand by that advice—blindly wrapping a massive REST API will poison your agent with context pollution and atomic operations.</p> <p>But I was being a little tongue-in-cheek. The truth is, REST APIs are <em>fantastic</em> starting points for MCP servers. They provide working endpoints, real business logic, and documented interfaces. The problem was never the APIs themselves—it was our tooling for converting them thoughtfully.</p> <p>FastMCP 2.11 includes a completely rewritten OpenAPI parser that addresses the core issues:</p> <p><strong>Performance</strong>: The new parser uses single-pass schema processing with optimized memory usage. What used to take minutes now takes seconds, even for massive specs.</p> <p><strong>Maintainability</strong>: The old parser had become a maintenance nightmare with edge cases and special handling scattered throughout. The new architecture is clean, extensible, and actually understandable.</p> <p><strong>Thoughtful Defaults</strong>: Instead of blindly converting every endpoint, the new parser makes intelligent decisions about what tools make sense for agents, while still giving you full control to customize the conversion.</p> <p>The new parser is experimental and disabled by default, but you can enable it with:</p> <pre><code>export FASTMCP_EXPERIMENTAL_ENABLE_NEW_OPENAPI_PARSER=1 </code></pre> <p>We're being thoughtful about the rollout because we know teams depend on the existing behavior. But early testing shows dramatic improvements in both performance and output quality.</p> <h2>Context State: Memory for Your Tools</h2> <p>One more thing: FastMCP 2.11 introduces persistent state management across tool calls. This seemingly simple feature unlocks powerful new patterns for multi-step agent workflows:</p> <pre><code>from fastmcp import FastMCP, Context mcp = FastMCP() @mcp.tool def start_analysis(ctx: Context, dataset_id: str): """Begin analyzing a dataset.""" ctx.state["analysis_id"] = f"analysis_{dataset_id}" ctx.state["progress"] = 0 return f"Started analysis {ctx.state['analysis_id']}" @mcp.tool def check_analysis_progress(ctx: Context): """Check the progress of the current analysis.""" if "analysis_id" not in ctx.state: return "No analysis in progress" return f"Analysis {ctx.state['analysis_id']} is {ctx.state['progress']}% complete" </code></pre> <p>The state persists across tool calls within the same session, giving your agents memory and the ability to maintain context across complex, multi-step operations. Note that state is only persisted for the duration of the session on the in-memory context object!</p> <p>Happy engineering!</p> FastMCP 2.12: Easy Enterprise Authhttps://jlowin.dev/blog/fastmcp-2-12/https://jlowin.dev/blog/fastmcp-2-12/Auth to the racesWed, 03 Sep 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>We're excited to announce the release of <a href="https://github.com/jlowin/fastmcp/releases/tag/v2.12.0">FastMCP 2.12</a>, a major update that reflects a pivotal moment for the MCP ecosystem. As more developers move their servers from local experiments to production services, the community's needs have evolved. This release may be our largest and most ambitious yet, designed to provide the production-grade tooling this maturing ecosystem demands.</p> <p>The scope of this release is a direct result of our growing community. To help steer the project, I'm also thrilled to <a href="https://www.jlowin.dev/blog/fastmcp-bill-easton">welcome Bill Easton</a> to the core team as our first external maintainer. Bill's vision has been instrumental in shaping FastMCP, and this release includes several of his key contributions.</p> <p>&lt;Callout color="blue"&gt; <a href="https://github.com/jlowin/fastmcp">Give us a star on GitHub</a> or check out the updated docs at <a href="https://gofastmcp.com">gofastmcp.com</a>. &lt;/Callout&gt;</p> <h3>Easy OAuth Integrations</h3> <p>The MCP specification requires servers to use OAuth 2.1 with <strong>Dynamic Client Registration (DCR)</strong>, a modern standard where clients can register themselves automatically.[^1] In FastMCP 2.11, we shipped a fully DCR-compliant <a href="https://gofastmcp.com/integrations/authkit">solution</a> with our partners at WorkOS, using their excellent AuthKit product.</p> <p>However, we recognize the reality that many large enterprises rely on identity providers like GitHub, Google, or Azure that do not support DCR (yet?). This leaves a critical gap for teams who needed to integrate MCP with their existing, battle-tested identity infrastructure.</p> <p>FastMCP 2.12 closes that gap with the new <strong>OAuth Proxy</strong> interface. The proxy acts as a bridge, allowing your server to present a fully DCR-compliant interface to MCP clients, while seamlessly managing traditional OAuth flows with your non-DCR identity provider.</p> <p>What was once a complex, multi-hundred-line integration is now a few lines of configuration. The main OAuthProxy is quite configurable, and we've also shipped built-in support for the most requested providers:</p> <ul> <li><strong><a href="https://gofastmcp.com/integrations/github">GitHub</a></strong></li> <li><strong><a href="https://gofastmcp.com/integrations/google">Google</a></strong></li> <li><strong><a href="https://gofastmcp.com/integrations/azure">Azure</a></strong></li> <li><strong><a href="https://gofastmcp.com/integrations/workos">WorkOS</a></strong></li> </ul> <p>This means you can add enterprise-grade authentication to your MCP server in seconds, not weeks.</p> <p>For example, here's how quickly you can add GitHub authentication to your server (assuming you have a GitHub OAuth app configured):</p> <pre><code>from fastmcp import FastMCP from fastmcp.server.auth.providers.github import GitHubProvider auth_provider = GitHubProvider( client_id="your_client_id", client_secret="your_client_secret", base_url="http://localhost:8000", # your server's URL ) mcp = FastMCP(name="GitHub Secured MCP", auth=auth_provider) </code></pre> <p>To learn more, please see the new <a href="https://gofastmcp.com/servers/auth/oauth-proxy">OAuth Proxy documentation</a>.</p> <p>&lt;Callout color="green"&gt; We're especially grateful to the community members who helped us test and refine this feature. It's rapidly improving as we collect feedback about production environments. &lt;/Callout&gt;</p> <h3>A Blueprint for Deployment</h3> <p>With the launch of <strong><a href="https://fastmcp.cloud">FastMCP Cloud</a></strong>, our mission is to make deploying an MCP server as easy as building one. To bring that same simplicity and portability to everyone, we're introducing a standard way to describe a server deployment: the <code>fastmcp.json</code> file.</p> <p>This declarative manifest is the single source of truth for your server, defining:</p> <ul> <li><strong>Source (<code>WHERE</code>):</strong> The location of your server code.</li> <li><strong>Environment (<code>WHAT</code>):</strong> Its Python version and dependencies.</li> <li><strong>Deployment (<code>HOW</code>):</strong> Its runtime configuration, like transport and port.</li> </ul> <p>For example:</p> <pre><code>{ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", "source": { "path": "server.py", "entrypoint": "mcp" }, "environment": { "python": "&gt;=3.10", "dependencies": ["pandas", "requests"] }, "deployment": { "transport": "http", "port": 8000 } } </code></pre> <p>This is the foundation for a future of truly portable MCP servers definitions. While FastMCP Cloud uses a separate manifest today, you can expect it to adopt <code>fastmcp.json</code> in the near future, enabling validated, one-click deployments with all dependencies correctly managed. We also anticipate support for new sources and environments.</p> <p>Today, you can use <code>fastmcp run fastmcp.json</code> to run your server with all dependencies and your preferred transport from the command line, with no additional configuration required. CLI arguments are respected as configuration overrides.</p> <p>For full details, please see the <a href="https://gofastmcp.com/deployment/server-configuration">server configuration documentation</a>.</p> <p>&lt;Callout color="gray"&gt; Please note: this is a server-side analogue to the popular <code>mcp.json</code> configuration file, not an alternative to it. <code>mcp.json</code> tells an MCP client how to connect to a specific server; <code>fastmcp.json</code> is a declarative deployment configuration for running an MCP server. &lt;/Callout&gt;</p> <h3>Solving MCP's Chicken-and-Egg Problem</h3> <p>MCP has many advanced features like "sampling", in which a server can ask the client's LLM to perform a task. However, these features require support from both servers and clients and consequently face a classic chicken-and-egg problem: server authors won't implement the feature if clients don't support it, and vice-versa.</p> <p>Thanks to a fantastic contribution from our new maintainer, <strong>Bill Easton</strong>, FastMCP is breaking this cycle. Server authors can now define <strong>fallback sampling handlers</strong>. If a client doesn't support sampling, FastMCP uses a server-side completions API to fulfill the request. This lets you build sophisticated tools with advanced MCP features <em>today</em>, knowing they will work for all clients and helping push the entire ecosystem forward.</p> <p>For more information, please see the new <a href="https://gofastmcp.com/clients/sampling#sampling-fallback">Sampling Fallbacks documentation</a>. FastMCP 2.12 includes an experimental OpenAI sampling handler, with more coming.</p> <hr /> <p>All of these features—enterprise-grade auth, declarative deployments, and ecosystem-aware fallbacks—represent FastMCP's commitment to building a robust, production-ready framework for the entire MCP community.</p> <p>&lt;Callout color="gray"&gt;</p> <ul> <li><strong>Upgrade:</strong> <code>uv add fastmcp</code> or <code>pip install fastmcp --upgrade</code></li> <li><strong>Explore:</strong> Dig into the new <a href="/servers/auth/authentication">Authentication</a> and <a href="/deployment/server-configuration">Project Configuration</a> documentation.</li> <li><strong>Contribute:</strong> Check out the code and examples on <a href="https://github.com/jlowin/fastmcp">GitHub</a>. &lt;/Callout&gt;</li> </ul> <p>Happy engineering!</p> <p>[^1]: Technically, servers don't <em>have</em> to support DCR, but then they must provide alternative ways for clients to authenticate that require much more complex configuration or client control. See <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#dynamic-client-registration">https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#dynamic-client-registration</a> for details.</p> FastMCP 2.13: Storage, Security, and Scalehttps://jlowin.dev/blog/fastmcp-2-13/https://jlowin.dev/blog/fastmcp-2-13/Cache me if you canSat, 01 Nov 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro"; import { Image } from 'astro:assets'; import downloads_img from './downloads.png'; import stars_img from './star-history.png';</p> <p>When we <a href="/blog/fastmcp-2-12">shipped</a> FastMCP 2.12 with its new <a href="https://gofastmcp.com/servers/auth/oauth-proxy">OAuth proxy</a> on August 31st, something remarkable happened. Downloads exploded from 200,000 to a peak of <strong>1.25 million a day</strong>. The proxy, which bridges MCP's modern DCR requirement with enterprise identity providers like Google and Azure, clearly hit a nerve. In fact, last week FastMCP surpassed the official MCP SDK in GitHub stars, a validation of the community's demand for high-level, production-ready tooling.</p> <p>&lt;div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-8"&gt; &lt;Image src={downloads_img} alt="FastMCP Downloads" /&gt; &lt;Image src={stars_img} alt="FastMCP Star History" /&gt; &lt;/div&gt;</p> <p>With that kind of scale, you get a lot of feedback, fast.</p> <p>This is the world <strong>FastMCP 2.13</strong> was built for. It is one of our largest releases, focused entirely on the infrastructure required for production MCP servers: persistent storage, battle-tested security, and performance optimizations.</p> <p>&lt;Callout color="blue"&gt; <a href="https://github.com/jlowin/fastmcp">Star FastMCP on GitHub</a> or check out the updated docs at <a href="https://gofastmcp.com">gofastmcp.com</a>. &lt;/Callout&gt;</p> <h3>Battle-Tested Authentication</h3> <p>The massive adoption of the OAuth proxy meant the community immediately started battle-testing our auth implementation in real-world scenarios. We learned our original Azure provider only worked in the narrowest of cases; intrepid users helped us build a far more robust version. Others contributed a variety of new providers, with the result being that FastMCP now supports out-of-the-box authentication with:</p> <ul> <li><a href="https://gofastmcp.com/integrations/workos">WorkOS</a> and <a href="https://gofastmcp.com/integrations/authkit">AuthKit</a></li> <li><a href="https://gofastmcp.com/integrations/github">GitHub</a></li> <li><a href="https://gofastmcp.com/integrations/google">Google</a></li> <li><a href="https://gofastmcp.com/integrations/azure">Azure</a> (Entra ID)</li> <li><a href="https://gofastmcp.com/integrations/aws-cognito">AWS Cognito</a></li> <li><a href="https://gofastmcp.com/integrations/auth0">Auth0</a></li> <li><a href="https://gofastmcp.com/integrations/descope">Descope</a></li> <li><a href="https://gofastmcp.com/integrations/scalekit">Scalekit</a></li> <li><a href="https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification">JWTs</a></li> <li><a href="https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol">RFC 7662 token introspection</a></li> </ul> <p>And we're working with Supabase to add support for their new identity provider.</p> <p>More critically, I owe a huge thanks to MCP Core Committee member <strong><a href="https://den.dev">Den Delimarsky</a></strong> for responsibly disclosing two nuanced, MCP-specific vulnerabilities: a confused deputy attack and a related token security boundary issue. The fixes required some novel solutions, including having the proxy issue its own tokens and implementing a new consent screen for explicit client approval. Our OAuth implementation is now hardened, spec-compliant, and thanks to the community's scrutiny, ready for production.</p> <p>You can learn more about confused deputy attacks from an <a href="https://den.dev/blog/mcp-confused-deputy-api-management/">excellent post</a> on Den's blog, and I'll write a post on FastMCP's specific implementation soon.</p> <h3>First-Class State Management</h3> <p>The rapid evolution of our auth stack highlighted a critical need: a robust way to manage persistent state. OAuth proxies need to store encrypted tokens and session data to survive restarts and work in distributed deployments.</p> <p>To solve this, FastMCP maintainer <strong><a href="https://www.linkedin.com/in/williamseaston/">Bill Easton</a></strong> built <a href="https://github.com/strawgate/py-key-value">py-key-value</a>. This fantastic library is something I've long wished for in the Python ecosystem: a clean key-value store with portable backend support. Its real genius is the composable wrapper system that lets you layer encryption, TTLs, and caching onto <em>any</em> backend, from a local filesystem to Redis or Elasticsearch.</p> <p>It's so good, we've baked it into FastMCP's core. In 2.13, persistent storage is now built-in and enabled by default where appropriate, providing the foundation for stateful, production-ready MCP applications.</p> <h3>A Raft of Other Improvements</h3> <p>Beyond the headlines, this release is packed with features and fixes that came directly from community feedback:</p> <ul> <li><strong>Response Caching:</strong> The new <code>ResponseCachingMiddleware</code> provides an instant performance win for expensive, repeated tool and resource calls.</li> <li><strong>Server Lifespans:</strong> We fixed a long-standing point of confusion in the MCP SDK. <code>lifespan</code> now correctly refers to the <em>server</em> lifecycle (for things like DB connections), not the client session. This is a breaking change, but it's the correct one.</li> <li><strong>Pydantic Validation:</strong> We now use Pydantic for input validation, avoiding the SDK's overly-strict JSON Schema enforcement. This more flexible approach is familiar to Python developers and more forgiving of LLMs that might send an integer as a string.</li> <li><strong>Richer Context:</strong> The <code>Context</code> API has been expanded, allowing your tools and resources to interact with other MCP functionality from inside their own execution.</li> </ul> <h3>What's Next</h3> <p>FastMCP 2.13 marks the framework's evolution into a production-ready platform. It includes work from <strong>20 new contributors</strong>, and it's their production feedback that made these improvements possible. Thank you.</p> <p>Looking ahead, our next major release, FastMCP 2.14, will be our first to remove deprecated features since launching 2.0. This is a sign of maturity: we're cleaning up the API and solidifying the foundation for the long term.</p> <p>Happy engineering!</p> <p>&lt;Callout color="gray"&gt;</p> <ul> <li><strong>Upgrade:</strong> <code>uv add fastmcp</code> or <code>pip install fastmcp --upgrade</code></li> <li><strong>Explore:</strong> Check out the new <a href="https://gofastmcp.com/servers/storage-backends">Storage</a> and <a href="https://gofastmcp.com/servers/auth/oauth-proxy">OAuth</a> documentation.</li> <li><strong>Contribute:</strong> Check out the code and examples on <a href="https://github.com/jlowin/fastmcp">GitHub</a>. &lt;/Callout&gt;</li> </ul> Now Streaming: FastMCP 2.3https://jlowin.dev/blog/fastmcp-2-3-streamable-http/https://jlowin.dev/blog/fastmcp-2-3-streamable-http/The most-requested feature is finally here.Thu, 08 May 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p><strong>FastMCP 2.3</strong> was just released, including the most-requested feature <em>by a mile</em>: Streamable HTTP for both FastMCP servers and clients.</p> <p>&lt;Callout color="blue"&gt;</p> <p><a href="https://github.com/jlowin/fastmcp">Give us a star on GitHub</a> or dive into the updated docs at <a href="https://gofastmcp.com">gofastmcp.com</a>.</p> <p>&lt;/Callout&gt;</p> <p>Until now, if you wanted to run your FastMCP server over the web, Server-Sent Events (SSE) was the primary option. While SSE works, it has drawbacks including the need for long-lived, stateful connections and a complex interchange across multiple routes. In addition, not all hosting infrastructure is compatible with SSE. Streamable HTTP is a more modern, efficient approach that way to handle the back-and-forth of an MCP session, all neatly wrapped in familiar HTTP.</p> <p>In fact, Streamable HTTP is so important for the MCP ecosystem that it's now the default http transport for FastMCP.</p> <p>To get started, just tell <code>mcp.run()</code> to use the new <code>"streamable-http"</code> transport in your server script. You can optionally customize the <code>host</code>, <code>port</code>, or mount <code>path</code>, as needed:</p> <pre><code># my_server.py from fastmcp import FastMCP mcp = FastMCP(name="MyStreamingServer") @mcp.tool() def echo(message: str) -&gt; str: return f"Server echoes: {message}" if __name__ == "__main__": mcp.run( transport="streamable-http", host="127.0.0.1", # Optional: defaults to 127.0.0.1 port=8000, # Optional: defaults to 8000 path="/mcp" # Optional: defaults to /mcp ) </code></pre> <p>Run this file with <code>python my_server.py</code>, and your server will start listening for Streamable HTTP connections at <code>http://127.0.0.1:8000/mcp</code>. You can see more about configuring the server in the <a href="https://gofastmcp.com/deployment/running-server#streamable-http">deployment docs</a>.</p> <p>Connecting your FastMCP client is even simpler. If your server is running on Streamable HTTP, just provide the URL to the client and FastMCP will automatically attempt to connect with the appropriate transport:</p> <pre><code>import asyncio from fastmcp import Client async def main(): # FastMCP 2.3 will automatically infer Streamable HTTP for this URL client = Client("http://127.0.0.1:8000/mcp") async with client: await client.ping() print("Ping successful!") result = await client.call_tool("echo", {"message": "Hello Stream!"}) print(result[0].text) if __name__ == "__main__": asyncio.run(main()) </code></pre> <p>More details can be found in the <a href="https://gofastmcp.com/clients/transports">client transports documentation</a>.</p> <p>The Model Context Protocol (MCP) is all about standardizing how AI models interact with tools and data, and with Streamable HTTP, FastMCP makes it even easier to build and deploy those crucial interaction points on the web. I'm excited to see what you build with these new capabilities. As always, your feedback, issues, and contributions are welcome!</p> <p>&lt;Callout color="gray"&gt;</p> <p>Give FastMCP 2.3 a try:</p> <ul> <li>Upgrade: <code>uv add fastmcp</code> or <code>pip install fastmcp --upgrade</code></li> <li>Explore the <a href="https://gofastmcp.com/deployment/running-server#streamable-http">documentation on deploying Streamable HTTP</a></li> <li>Check out the code and examples on <a href="https://github.com/jlowin/fastmcp">GitHub</a></li> </ul> <p>&lt;/Callout&gt;</p> <p>Happy Streaming! 🌊</p> Blast Auth with FastMCP 2.6https://jlowin.dev/blog/fastmcp-2-6/https://jlowin.dev/blog/fastmcp-2-6/Real-world authentication for MCP servers and clientsMon, 02 Jun 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>FastMCP's journey continues at a thrilling pace. Since my <a href="/blog/fastmcp-2-3-streamable-http">last update on Streamable HTTP</a>, we've rolled out <a href="https://github.com/jlowin/fastmcp/releases/tag/v2.4.0">version 2.4 ("Config and Conquer")</a> to simplify MCP client configuration, and <a href="https://github.com/jlowin/fastmcp/releases/tag/v2.5.0">version 2.5 ("Route Awakening")</a> with powerful new tools for OpenAPI generation.</p> <p>As the team and community around FastMCP grows, so does our commitment to rapid iteration. Our mission is clear: <strong>deliver the simplest path to production</strong> in the MCP ecosystem. This means shipping developer-friendly features that drive real-world adoption and partnering with best-in-class providers to make distribution effortless. Much more on that front coming soon!</p> <p>Today, I'm incredibly excited to announce <strong>FastMCP 2.6 ("Blast Auth")</strong>. This release is a game-changer because it tackles a critical need that has, almost overnight, become paramount: authentication for remote MCP servers.</p> <p>The timing here is no accident.</p> <p>In just the last week, there's been a whirlwind of major MCP activity. Industry leaders like <a href="https://www.anthropic.com/news/agent-capabilities-api">Anthropic</a>, <a href="https://openai.com/index/new-tools-and-features-in-the-responses-api/">OpenAI</a>, and <a href="https://blog.google/technology/google-deepmind/google-gemini-updates-io-2025/">Google</a> all announced support for accessing remote MCP servers directly within their APIs and SDKs. This is a <em>massive</em> step forward and a resounding validation of the MCP vision, signaling a serious industry-wide commitment to standardizing how AI models interact with tools and data.</p> <p><strong>But with great power (and public endpoints) comes great responsibility.</strong></p> <p>Many of these new API integrations hinge on the LLMs being able to access your MCP servers remotely. And let's be frank: no one wants to expose an unauthenticated MCP server to the public internet, especially if it’s a gateway to sensitive internal systems. Therefore, MCP authentication has swiftly moved from a "nice-to-have" to an absolute necessity.</p> <p>This is where the current landscape gets a bit... interesting.</p> <p>The official MCP specification, in its wisdom, dictates that HTTP-based MCP servers <em>must</em> implement a full OAuth 2.1 handshake. That's a robust standard, no doubt. But it’s also a heavy lift designed for interactive, browser-based use cases, and one that's difficult to manage in the programmatic, server-to-server use cases that these new API integrations are designed for.</p> <p>So while the big API providers are saying "Yes, bring your MCP servers!" and allowing users to <em>provide</em> access tokens for those servers, they're punting on exactly <em>how</em> those tokens should be obtained, essentially saying, "That's your problem."</p> <p>Happily, FastMCP 2.6 is here to solve it by introducing straightforward server and client authentication.</p> <p>We have taken a decidedly pragmatic approach with our first cut of server-side auth and shipped a Bearer token authentication scheme. We want to be clear that this does not implement a full OAuth 2.1 handshake, and therefore is <strong>not strictly compliant with the MCP spec</strong>. However, it is fully compatible with how the major AI vendors are actually using MCP today, and allows users to begin shipping useful applications immediately.</p> <p>Setting up a full OAuth 2.1 identity server is a significant undertaking more appropriate for enterprise production than a gradual developer adoption curve. Bearer token validation, by contrast, can be as simple as providing a public key. This means you can secure your FastMCP server with minimal friction and get back to building cool things.</p> <p>Frankly, this feels like an area where the MCP spec might be running well ahead of its own maturity.</p> <p>In FastMCP 2.6, you can either provide a public key directly to your server (in PEM format) or use a JWKS URI to fetch the key(s) dynamically from a remote server:</p> <p>&lt;figure&gt;</p> <pre><code>from fastmcp import FastMCP from fastmcp.server.auth import BearerAuthProvider mcp = FastMCP( name="MyAuthenticatedServer", auth=BearerAuthProvider( # -- Provide a static public key (PEM format) # public_key="your-public-key-string", # -- OR, preferably for production, a JWKS URI jwks_uri="https://example.com/.well-known/jwks.json", ) ) @mcp.tool() def echo(message: str) -&gt; str: return f"Server echoes: {message}" if __name__ == "__main__": mcp.run(transport="streamable-http") </code></pre> <p>&lt;figcaption&gt;A simple FastMCP server with Bearer authentication.&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>To learn more, please review the <a href="https://gofastmcp.com/servers/auth/bearer">server-side auth docs</a>.</p> <p><em>A quick note: We're already collaborating with partners to bring plug-and-play OAuth 2.1 server integrations to FastMCP. Bearer auth is only the first, pragmatic step to meet the ecosystem's immediate needs.</em></p> <p>On the client side, however, we've pulled out all the stops.</p> <p>The FastMCP client now boasts comprehensive support for <strong>both</strong> Bearer token authentication (simply provide your token) <em>and</em> a remarkably smooth, <strong>full in-browser OAuth 2.1 flow</strong>.</p> <p>For many OAuth-protected servers, our client can navigate the entire browser-based handshake with minimal, and sometimes <em>zero</em>, additional configuration on your part. After wading through the intricacies of auth protocols these past few months, seeing this "just work" feels like a genuine breakthrough for developer experience.</p> <p>To use the client with default OAuth settings and dynamic registration, it's as simple as passing the string <code>"oauth"</code> as the auth parameter:</p> <pre><code>import asyncio from fastmcp import Client client = Client( "http://my-secure-oauth-server.com/mcp", # URL to your MCP server auth="oauth", # Enable OAuth with default settings ) async def main(): async with client: await client.ping() print("Successfully connected with OAuth!") if __name__ == "__main__": asyncio.run(main()) </code></pre> <p>This enables your FastMCP client applications to securely interact with a wide array of OAuth-protected MCP servers, with FastMCP elegantly handling the underlying complexities.</p> <p>You can learn more about customizing client-side auth in the <a href="https://gofastmcp.com/clients/auth/oauth">client auth docs</a>.</p> <p>To help you hit the ground running, we've also shipped <strong>four new tutorials</strong> demonstrating how to integrate your FastMCP servers with <a href="https://gofastmcp.com/integrations/anthropic">Anthropic's API</a>, <a href="https://gofastmcp.com/integrations/claude-desktop">Claude Desktop</a>, <a href="https://gofastmcp.com/integrations/openai">OpenAI's API</a>, and the <a href="https://gofastmcp.com/integrations/gemini">Gemini SDK</a>. These guides make it easier than ever to connect your secure FastMCP servers to the world's leading AI models.</p> <p>This release, and our approach to authentication in particular, exemplifies how FastMCP 2.0 is committed to making high-level, opinionated decisions that prioritize developer experience and enable rapid, practical deployment. <strong>We're building the toolkit we want for working with MCP in the real world.</strong></p> <p>And speaking of real-world deployment: we know that securing your server is only half the battle; you also need a place to host it. We've got some very exciting news on that front coming very, very soon...</p> <p>For now, dive into FastMCP 2.6!</p> <p>&lt;Callout color="gray"&gt;</p> <ul> <li>Upgrade: <code>uv add fastmcp</code> or <code>pip install fastmcp --upgrade</code></li> <li>Explore the documentation on <a href="https://gofastmcp.com/servers/auth/bearer">server</a> and <a href="https://gofastmcp.com/clients/auth/oauth">client</a> authentication.</li> <li>Check out the code and examples on <a href="https://github.com/jlowin/fastmcp">GitHub</a>. &lt;/Callout&gt;</li> </ul> <p>Happy Authenticating! 🔒</p> FastMCP 2.8: Transform and Roll Outhttps://jlowin.dev/blog/fastmcp-2-8-tool-transformation/https://jlowin.dev/blog/fastmcp-2-8-tool-transformation/More than meets the APIWed, 11 Jun 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>What do you do when the perfect tool... isn't so perfect? You've found a library or an API that does exactly what you need, but its interface is a nightmare for an LLM. The argument names are cryptic, the descriptions are missing, and it exposes parameters you'd rather keep hidden.</p> <p>Today, we're thrilled to announce <strong>FastMCP 2.8</strong>, a massive release that starts to put the power of curation directly in your hands. This update is all about giving you fine-grained control to transform, filter, and shape the components your AI interacts with, and it's the foundation for a lot of the new features we're working on.</p> <p>&lt;Callout color="blue"&gt; <a href="https://github.com/jlowin/fastmcp">Give us a star on GitHub</a> or check out the updated docs at <a href="https://gofastmcp.com">gofastmcp.com</a>. &lt;/Callout&gt; &lt;br/&gt; &lt;Callout color="green"&gt; Also! The waitlist is open for <a href="https://fastmcp.cloud">FastMCP Cloud</a>, which is literally the fastest way to get started with MCP. More on that very soon. &lt;/Callout&gt;</p> <h3>🛠️ Tool Transformation: Curate the LLM Experience</h3> <p>The highlight of this release is first-class <strong><a href="https://gofastmcp.com/patterns/tool-transformation">Tool Transformation</a></strong>. Instead of wrestling with complex prompts to make an LLM use a clunky tool, you can now adapt the tool itself to be perfectly LLM-friendly.</p> <p>This feature was developed in close partnership with <strong><a href="https://www.linkedin.com/in/williamseaston/">Bill Easton</a></strong> of Elastic, who has become one of FastMCP's most prolific contributors and a key thought partner. As Bill brilliantly <a href="https://www.linkedin.com/feed/update/urn:li:activity:7338011349525983232/">put it</a>:</p> <blockquote> <p>Tool transformation flips Prompt Engineering on its head: stop writing tool-friendly LLM prompts and start providing LLM-friendly Tools.</p> </blockquote> <p>With a single <code>Tool.from_tool()</code> call, you can now create enhanced variations of any tool—whether it's from your own codebase, a third-party library, or an auto-generated OpenAPI server.</p> <ul> <li><strong>Rename</strong> arguments to be more intuitive (<code>q</code> becomes <code>search_query</code>).</li> <li><strong>Rewrite</strong> descriptions to give the LLM better context.</li> <li><strong>Hide</strong> parameters like API keys, providing default values behind the scenes.</li> <li><strong>Wrap</strong> a tool with custom validation or post-processing logic.</li> </ul> <pre><code>from fastmcp import FastMCP from fastmcp.tools import Tool from fastmcp.tools.tool_transform import ArgTransform mcp = FastMCP() # An existing, generic tool from a third party from some_library import generic_search # Transform it into a domain-specific, LLM-friendly tool product_search = Tool.from_tool( tool=generic_search, name="find_products_by_keyword", description="Searches the product catalog for items matching a keyword.", transform_args={ "q": ArgTransform( name="keyword", description="The search term for finding products.", ), "limit": ArgTransform(hide=True, default=10) # Hide and pass along a new default } ) mcp.add_tool(product_search) </code></pre> <p>This is a foundational step towards a future where we don't just provide tools, but actively <em>curate</em> the LLM's environment, paving the way for more sophisticated agentic systems.</p> <h3>🫥 Enabling and Disabling Components</h3> <p>Now that you've transformed a tool into a sleek, LLM-friendly powerhouse, you'll probably want to hide the old, busted original. This release introduces a simple way to manage component visibility.</p> <p>Every tool, resource, and prompt can now be programmatically enabled or disabled. You can set the initial state in the decorator or toggle it at runtime.</p> <pre><code>@mcp.tool(enabled=False) def legacy_tool(): """This tool is disabled from the start.""" # ... # you can enable it later legacy_tool.enable() # or turn it back off legacy_tool.disable() </code></pre> <p>This gives you precise control to roll out new features, deprecate old ones, or dynamically adjust the toolset available to your clients.</p> <h3>🏷️ Component Control: Tags Have a Purpose!</h3> <p>FastMCP introduced component tags all the way back in v2.1.0, and since then users have been asking: "What are these for?" Today, we're excited to finally have an answer:</p> <p><strong>Tag-based filtering</strong> is here, allowing you to declaratively control which components are exposed based on the tags you assign.</p> <pre><code>mcp = FastMCP( name="MyFilteredServer", # Only expose components with the "public" tag include_tags={"public"}, # But exclude any that are also tagged "beta" exclude_tags={"beta"} ) @mcp.tool(tags={"public"}) def stable_feature(): """This tool is public and will be exposed.""" # ... @mcp.tool(tags={"public", "beta"}) def new_feature(): """This tool is public but also beta, so it will be excluded.""" # ... </code></pre> <p>This is perfect for managing different environments (e.g., exposing <code>internal</code> tools in dev but not prod) or controlling access for different user types.</p> <h3>🔀 A Pragmatic Shift for OpenAPI</h3> <p>In our commitment to providing the simplest path to production, we sometimes have to make pragmatic decisions. This release includes a minor but important <strong>breaking change</strong> to FastMCP's default OpenAPI route maps. To improve out-of-the-box compatibility with the widest range of LLM clients, all API endpoints from an OpenAPI spec are now converted to <code>Tools</code> by default.</p> <p>Previously, <code>GET</code> requests were mapped to either resources or resource templates as appropriate. However, the reality is that most MCP clients available today (including all major foundation model vendors... looking at you, Anthropic) only support MCP tools and essentially disregard every other feature.</p> <p>While we could wait for them to adopt full-featured clients (I know a <a href="https://gofastmcp.com/clients/client">great library</a>...), we've decided to make the pragmatic shift to tools-only in order to ensure that our users don't have to do extra work.</p> <p>For users who need the previous semantic behavior, it can be easily restored by providing a custom <code>route_maps</code> configuration, as detailed in the <a href="https://gofastmcp.com/servers/openapi#custom-route-maps">OpenAPI docs</a>.</p> <p>Alongside these headline features, v2.8.0 continues the major modernization effort we began in v2.7, with a host of internal improvements and optimizations to make FastMCP more robust and performant.</p> <p>FastMCP 2.8 puts more power and control in your hands than ever before. We're excited to see how you use these new features to build even more sophisticated and robust MCP applications.</p> <p>&lt;Callout color="gray"&gt;</p> <ul> <li>Upgrade: <code>uv add fastmcp</code> or <code>pip install fastmcp --upgrade</code></li> <li>Explore the documentation on <a href="https://gofastmcp.com/patterns/tool-transformation">Tool Transformation</a>, <a href="https://gofastmcp.com/servers/fastmcp#tag-based-filtering">Tag-based Filtering</a>, and <a href="https://gofastmcp.com/servers/tools#disabling-tools">Enabling/Disabling Components</a>.</li> <li>Check out the code and examples on <a href="https://github.com/jlowin/fastmcp">GitHub</a>.</li> <li>Sign up for <a href="https://fastmcp.cloud">FastMCP Cloud</a>. &lt;/Callout&gt;</li> </ul> <p>Happy Transforming! 🤖</p> MCP-Native Middleware with FastMCP 2.9https://jlowin.dev/blog/fastmcp-2-9-middleware/https://jlowin.dev/blog/fastmcp-2-9-middleware/Stuck in the middleware with youMon, 23 Jun 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>This morning we released FastMCP 2.9, which includes a new, MCP-native approach to middleware.</p> <p>&lt;Callout color="blue"&gt; <a href="https://github.com/jlowin/fastmcp">Give us a star on GitHub</a> or check out the updated docs at <a href="https://gofastmcp.com">gofastmcp.com</a>. &lt;/Callout&gt;</p> <p><strong>Middleware</strong> is one of those foundational features we've come to expect from any serious server framework. It's the go-to pattern for adding cross-cutting concerns like authentication, logging, or caching without rewriting your core application logic.</p> <p>Until today, when developers asked how to add middleware to their MCP server, the obvious answer seemed to be wrapping their server with traditional ASGI middleware. Unfortunately, that approach has two critical flaws:</p> <ol> <li> <p>It only works for web-based transports like streamable-HTTP and SSE. Until very recently, most major clients only supported the local STDIO transport, making this a non-starter for many.</p> </li> <li> <p>More importantly, it forces you to parse the MCP's low-level JSON-RPC messages yourself. All the hard work FastMCP does to give you clean, high-level Tool and Resource objects is lost. You're left trying to reconstruct meaning from a sea of protocol noise.</p> </li> </ol> <p>This is a lot of work for a very limited set of outcomes.</p> <p>So, we went back to the drawing board and embraced a core FastMCP principle: <strong>focus on the developer's intent, not the protocol's complexity.</strong></p> <h2>MCP-Native Middleware</h2> <p>FastMCP 2.9 introduces a powerful, intuitive middleware system. Instead of wrapping the raw protocol stream, we wrap the high-level, semantic handlers that developers interact with. This is middleware that understands <code>tools</code>, <code>resources</code>, and <code>prompts</code>, not just JSON-RPC messages.</p> <p>Creating middleware is as simple as subclassing <code>fastmcp.server.middleware.Middleware</code> and overriding the hooks you need. Here's a basic logging middleware that prints every request and response (if any):</p> <pre><code>from fastmcp import FastMCP from fastmcp.server.middleware import Middleware, MiddlewareContext class LoggingMiddleware(Middleware): async def on_message(self, context: MiddlewareContext, call_next): """Called for every MCP message.""" print(f"-&gt; Received {context.method}") result = await call_next(context) print(f"&lt;- Responded to {context.method}") return result mcp = FastMCP(name="My Server") mcp.add_middleware(LoggingMiddleware()) </code></pre> <p>While <code>on_message</code> is great for generic tasks, the true strength of FastMCP's middleware lies in its semantic awareness. You can target specific protocol messages types with <code>on_request</code> or <code>on_notification</code>, further filtered by whether the request was initiated by the server or the client, or even target specific operations like <code>on_call_tool</code> to implement more sophisticated logic.</p> <p>For example, here's a middleware that prevents access to tools tagged as <code>"private"</code>:</p> <pre><code>from fastmcp import FastMCP, Context from fastmcp.exceptions import ToolError from fastmcp.server.middleware import Middleware, MiddlewareContext class PrivateMiddleware(Middleware): async def on_call_tool(self, context: MiddlewareContext, call_next): """Called when a tool is called.""" # Fetch the FastMCP Tool object tool_name = context.message.name tool = await context.fastmcp_context.fastmcp.get_tool(tool_name) # Check if the tool is tagged as private if "private" in tool.tags: raise ToolError(f"Access denied to private tool: {tool_name}") # If the check passes, continue to the next handler return await call_next(context) mcp = FastMCP(name="Private Server") @mcp.tool(tags={"private"}) def super_secret_function(): return "This is a secret!" mcp.add_middleware(PrivateMiddleware()) </code></pre> <p>This approach leverages FastMCP's high-level understanding of your components to enable powerful, context-aware logic for authentication, authorization, caching, and more.</p> <p>To get you started, in addition to the core <code>Middleware</code> class, we've added a few starting templates for common middleware patterns:</p> <ul> <li><code>fastmcp.server.middleware.logging</code>: Logs every request and notification.</li> <li><code>fastmcp.server.middleware.error_handling</code>: Catch and retry errors.</li> <li><code>fastmcp.server.middleware.rate_limiting</code>: Limits the rate of requests.</li> <li><code>fastmcp.server.middleware.timing</code>: Basic performance monitoring.</li> </ul> <p>Check out the full <a href="https://gofastmcp.com/servers/middleware">middleware documentation</a> to see what's possible.</p> <h2>But Wait, There's More</h2> <p>FastMCP 2.9 is a huge release, and it also includes one highly-requested feature: <strong>server-side type conversion for prompt arguments.</strong></p> <p>The MCP spec requires all prompt arguments to be strings. This has been a persistent developer pain point. Why? Because the Python function that generates those prompts often needs structured data to perform business logic, such as a list of IDs to look up, a dictionary of configuration, or some filter criteria. This has forced developers to litter their prompt logic with <code>json.loads()</code> and pray that the agent provides a compatible input.</p> <p>Not anymore.</p> <p>With FastMCP 2.9, you can define your prompt functions with the native Python types you'd expect. FastMCP automatically handles the conversion from string to type on the server. Crucially, it also enhances the prompt's description to show clients the expected JSON schema format, making it clear how to provide structured data. And to complete the story, FastMCP <code>Clients</code> will now automatically serialize non-string arguments for you.</p> <pre><code>from fastmcp import FastMCP import inspect mcp = FastMCP() @mcp.prompt def analyze_users( user_ids: list[int], # Auto-converted from JSON! analysis_type: str, ) -&gt; str: """Generate analysis prompt using loaded user data.""" users = [] for user_id in user_ids: user = db.get_user(user_id) # pseudocode users.append(f"- {user_id}: {user.name}, {user.metrics}") user_data = "\n".join(users) return inspect.cleandoc( f""" Analyze these users for {analysis_type} insights: {user_data} Provide actionable recommendations. """ ) </code></pre> <p>An MCP client would call this with <code>{"user_ids": "[1, 2, 3]", "analysis_type": "performance"}</code>, but the MCP server would receive a clean <code>list</code> and <code>str</code>. It's a small change that removes a huge amount of friction, especially when prompts are doing more than just string interpolation.</p> <p>FastMCP's implementation of this feature is fully MCP spec-compliant, but because there is no <em>formal</em> way to describe the expected JSON Schema format of a prompt argument, it's possible that some clients will choose to ignore it. As with all agentic users, performance will depend on clarity of your instructions.</p> <h2>From Protocol to Framework</h2> <p>With features like middleware and automatic type conversion, FastMCP is evolving beyond a simple high-level protocol implementation. It's becoming a true application framework: an opinionated, high-level toolkit for building sophisticated, production-ready MCP applications. Our goal remains the same: <strong>to provide the simplest path to production</strong>.</p> <p>&lt;Callout color="gray"&gt;</p> <ul> <li><strong>Upgrade:</strong> <code>uv add fastmcp</code> or <code>pip install fastmcp --upgrade</code></li> <li><strong>Explore:</strong> Dig into the new <a href="/servers/middleware">Middleware</a> and <a href="/servers/prompts">Prompts</a> documentation.</li> <li><strong>Contribute:</strong> Check out the code and examples on <a href="https://github.com/jlowin/fastmcp">GitHub</a>. &lt;/Callout&gt;</li> </ul> <p>Happy engineering!</p> FastMCP Context Switchinghttps://jlowin.dev/blog/fastmcp-context-switching/https://jlowin.dev/blog/fastmcp-context-switching/Seamless access for all componentsWed, 30 Apr 2025 00:00:00 GMT<p>Previously in FastMCP, the powerful <code>Context</code> object – the gateway to MCP features like logging, progress reporting, resource access, and client LLM sampling – was only easily accessible in MCP tool functions. While tools are central to MCP, this limited where you could add dynamic, session-aware logic.</p> <p>Starting with <strong>FastMCP 2.2.5</strong>, the <code>Context</code> object is now available across <em>all</em> FastMCP components! You can now seamlessly inject and use context within:</p> <ul> <li>Tools (<code>@tool()</code>)</li> <li>Resources (<code>@resource("resource://user")</code>)</li> <li>Resource templates (e.g., <code>@resource("resource://users/{user_id}")</code>)</li> <li>Prompt functions (<code>@prompt()</code>)</li> </ul> <p>In all cases, the pattern is the same: add a keyword argument to your decorated function and give it a type hint of <code>Context</code>. FastMCP will detect the annotation and inject the correct context automatically.</p> <pre><code>from fastmcp import FastMCP, Context mcp = FastMCP(name="ContextDemo") @mcp.tool() async def add(a: int, b: int, ctx: Context) -&gt; int: # ctx will be automatically injected by FastMCP await ctx.debug(f"Adding {a} and {b}") return a + b </code></pre> <p>In addition to logging, <code>Context</code> allows you to take advantage of powerful features like client LLM sampling.</p> <p>For more details, see the FastMCP <a href="https://gofastmcp.com/servers/context">Context documentation</a>.</p> Introducing FastMCP 2.0 🚀https://jlowin.dev/blog/fastmcp-2/https://jlowin.dev/blog/fastmcp-2/Composing the AI EcosystemWed, 16 Apr 2025 00:00:00 GMT<p>I'm thrilled to announce the release of <strong>FastMCP 2.0</strong>! 🎉</p> <p><strong><a href="https://github.com/jlowin/fastmcp">Give it a star</a></strong> or check out the docs at <strong><a href="https://gofastmcp.com">gofastmcp.com</a></strong>.</p> <h2>🚀 FastMCP 2.0</h2> <p>The <a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a> aims to be the "USB-C port for AI," providing a standard way for large language models to interact with data and tools. FastMCP's mission has always been to make implementing this protocol as fast, simple, and Pythonic as possible.</p> <p>FastMCP 1.0 was incredibly successful – so much so that its core SDK is now included in the official <a href="https://github.com/modelcontextprotocol/python-sdk">MCP Python SDK</a>! You can <code>from mcp.server.fastmcp import FastMCP</code> and be up and running in minutes.</p> <p>However, when I wrote the first version of FastMCP, the MCP itself was only a week old. I <a href="/blog/introducing-fastmcp">introduced FastMCP</a> with the tagline "because life's too short for boilerplate," focusing on making it easy to create MCP servers without getting bogged down in protocol details.</p> <p>A few months later, the MCP ecosystem has matured. If FastMCP 1.0 was about easily <em>creating</em> servers, then FastMCP 2.0 is about easily <em>working with</em> them. This required a significant rewrite, which is why we're back in a standalone project, but v2 is backwards-compatible with v1 while introducing powerful new features for composition, integration, and interaction.</p> <p>Here’s what’s new:</p> <h3>🧩 Compose Servers with Ease</h3> <p>You can now build modular applications by combining multiple FastMCP servers together, optionally using prefixes to avoid naming collisions.</p> <p>You can either <code>mount</code> a local or remote server to live-link it to your server, exposing its components while forwarding all requests, or use <code>import_server</code> to statically copy another server's resources and tools into your own. See the <a href="https://gofastmcp.com/patterns/composition">composition docs</a> for more.</p> <pre><code>from fastmcp import FastMCP # Define subservers (e.g., weather_server, calc_server) weather_server = FastMCP(name="Weather") @weather_server.tool() def get_forecast(city: str): return f"Sunny in {city}" calc_server = FastMCP(name="Calculator") @calc_server.tool() def add(a: int, b: int): return a + b main_app = FastMCP(name="MainApp") # Mount the subservers main_app.mount("weather", weather_server) main_app.mount("calc", calc_server) # main_app now dynamically exposes `weather_get_forecast` and `calc_add` if __name__ == "__main__": main_app.run() </code></pre> <h3>🔄 Proxy Any MCP Server</h3> <p>Composition is great for combining servers you control, but what about interacting with third-party servers, remote servers, or those not built with FastMCP?</p> <p>FastMCP can now proxy any MCP server, turning it into a FastMCP server that's compatible with all other features, including composition.</p> <p>The killer feature? <strong>You're no longer locked into the backend server's transport.</strong> The proxy can run using <code>stdio</code>, <code>sse</code>, or any other FastMCP-supported transport, regardless of how the backend is hosted.</p> <p>For more information, see the <a href="https://gofastmcp.com/patterns/proxy">proxying docs</a>.</p> <pre><code>from fastmcp import FastMCP, Client # Point a client at *any* backend MCP server (local FastMCP instance, remote SSE, local script...) backend_client = Client("http://api.example.com/mcp/sse") # e.g., a remote SSE server proxy_server = FastMCP.from_client(backend_client, name="MyProxy") # Run the proxy locally via stdio (useful for Claude Desktop, etc.) if __name__ == "__main__": proxy_server.run() # Defaults to stdio </code></pre> <h3>🪄 Auto-Generate Servers from OpenAPI &amp; FastAPI</h3> <p>Many developers want to make their existing REST APIs accessible to LLMs without reinventing the wheel. FastMCP 2.0 makes this trivial by automatically generating MCP servers from OpenAPI specs or FastAPI apps.</p> <p>Explore the <a href="https://gofastmcp.com/patterns/openapi">OpenAPI</a> and <a href="https://gofastmcp.com/patterns/fastapi">FastAPI</a> guides for more.</p> <pre><code>from fastapi import FastAPI from fastmcp import FastMCP # Your existing FastAPI app fastapi_app = FastAPI() @fastapi_app.get("/items/{item_id}") def get_item(item_id: int): return {"id": item_id, "name": f"Item {item_id}"} # Generate an MCP server mcp_server = FastMCP.from_fastapi(fastapi_app) # Run the MCP server (exposes FastAPI endpoints as MCP tools/resources) if __name__ == "__main__": mcp_server.run() </code></pre> <h3>🧠 Client Infrastructure &amp; LLM Sampling</h3> <p>FastMCP 2.0 introduces a completely <strong>new client infrastructure</strong> designed for robust interaction with any MCP server, supporting all major transports and even in-memory transport when working with local FastMCP servers.</p> <p>This makes it easy to expose advanced MCP features like <strong>client-side LLM sampling</strong>. Tools running <em>on the server</em> can now ask the <em>client's</em> LLM to perform tasks using <code>ctx.sample()</code>. Imagine a server tool that fetches complex data and then asks the LLM connected to the client (like Claude or ChatGPT) to summarize it before returning the result.</p> <pre><code>from fastmcp import FastMCP, Context mcp = FastMCP(name="SamplingDemo") @mcp.tool() async def analyze_data_with_llm(data_uri: str, ctx: Context) -&gt; str: """Fetches data and uses the client's LLM for analysis.""" # log to the client's console await ctx.info(f"Fetching data from {data_uri}...") data_content = await ctx.read_resource(data_uri) # Simplified await ctx.info("Requesting LLM analysis...") # Ask the connected client's LLM to analyze the data analysis_response = await ctx.sample( f"Analyze the key trends in this data:\n\n{data_content[:1000]}" ) return analysis_response # Return the LLM's analysis``` </code></pre> <p>This unlocks sophisticated workflows where server-side logic collaborates with the client-side LLM's intelligence. For more information, see the updated <a href="https://gofastmcp.com/clients/client">Client</a> and <a href="https://gofastmcp.com/servers/context">Context</a> guides.</p> <h2>🏗️ Building the MCP Ecosystem</h2> <p>FastMCP 2.0 is a major step towards a more connected, flexible, and developer-friendly AI ecosystem built on MCP. By simplifying proxying, composition, and integration, we hope to empower you to build and combine MCP services in powerful new ways.</p> <p>Give FastMCP 2.0 a try!</p> <ul> <li>Explore the <strong><a href="https://gofastmcp.com">documentation</a></strong></li> <li>Check out the code and examples on <strong><a href="https://github.com/jlowin/fastmcp">GitHub</a></strong></li> <li>Add it to your poject: <code>uv add fastmcp</code> or <code>pip install fastmcp</code></li> </ul> <p>I'm excited to see what you build. Your feedback, issues, and contributions are always welcome!</p> <p>Happy Engineering!</p> What's New in FastMCP 3.0https://jlowin.dev/blog/fastmcp-3-whats-new/https://jlowin.dev/blog/fastmcp-3-whats-new/A comprehensive guide to every major featureTue, 20 Jan 2026 13:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p><em>FastMCP 3.0 is our <a href="/blog/fastmcp-3">largest release ever</a>. This post covers every major feature in some detail, including an overview of the architecture and links to relevant documentation. For beta 2 features (CLI toolkit, MCP Apps, CIMD, and more), see the <a href="/blog/fastmcp-3-beta-2">Beta 2 announcement</a>.</em></p> <h2>The Architecture</h2> <p>FastMCP 2 had features. Lots of them. Mounting servers, proxying remotes, filtering by tags, transforming tool schemas. Each feature was its own subsystem with its own code, its own mental model, its own edge cases. When you wanted to add something new, you had to figure out how it interacted with everything else—and the answer was usually "write more glue code."</p> <p>FastMCP 3 asks a different question: what if all of these features are just different combinations of the same primitives?</p> <p>The architecture comes down to three concepts:</p> <p><strong>Components</strong> are the atoms of MCP. A tool, a resource, a prompt. They're what clients actually interact with. Components have names, schemas, metadata, and behavior. They're the thing you're ultimately trying to expose.</p> <p><strong>Providers</strong> answer the question: where do components come from? A provider is anything that can list components and retrieve them by name. Your decorated functions are a provider. A directory of files is a provider. A remote MCP server is a provider. An OpenAPI spec is a provider. Critically, a FastMCP server is <em>itself</em> a provider—which means you can nest servers inside servers, infinitely.</p> <p><strong>Transforms</strong> are middleware for the component pipeline. They intercept the flow of components from providers to clients and can modify what passes through. Rename a tool, add a namespace prefix, filter by version, hide components by tag—these are all transforms. Transforms compose: you stack them, and each one processes the output of the previous.</p> <h3>Why Composability Matters</h3> <p>Here's where it gets interesting. In FastMCP 2, "mounting" a sub-server was a massive specialized feature. Hundreds of lines of code to handle the namespacing, the middleware chains, the lifecycle management. Same story for proxying remote servers. Same story for visibility filtering.</p> <p>In FastMCP 3, mounting is just two primitives combined:</p> <ul> <li>A <strong>Provider</strong> that sources components from another server</li> <li>A <strong>Transform</strong> that adds a namespace prefix</li> </ul> <p>That's it. There's no special mounting code. The mounting behavior <em>emerges</em> from the composition of primitives that each do one thing well.</p> <p>Proxying a remote server? That's a Provider backed by an MCP client. The Provider wraps the client, translates list/get calls into MCP protocol calls, and returns the results. No special proxy subsystem—just a provider that happens to talk to a remote server.</p> <p>Per-session visibility, where different users see different tools? That's a Transform applied to an individual session instead of the server. The visibility transform doesn't know or care whether it's running globally or per-session. It just filters components based on rules. The per-session behavior comes from <em>where</em> you apply it.</p> <p>This composability has a practical consequence: FastMCP 3 ships more features with less code, and you can combine features in ways we didn't anticipate. Want to proxy a remote server, filter its tools by tag, rename them, and expose them only to authenticated users? That's a Provider, three Transforms, and some auth middleware. Each piece is independent. Each piece is testable. And when we add new transforms or providers, they automatically work with everything else.</p> <h3>How It Actually Works</h3> <p>When a client asks for the list of tools, here's what happens:</p> <ol> <li>The server collects components from all its Providers</li> <li>Each Provider runs its own transform chain (provider-level transforms)</li> <li>The server runs its transform chain on the aggregated result (server-level transforms)</li> <li>The final list goes to the client</li> </ol> <p>This two-level transform system is powerful. Provider-level transforms affect only that provider's components—useful for namespacing a mounted server. Server-level transforms affect everything—useful for global visibility rules or auth filtering.</p> <p>The same flow happens for <code>get_tool</code>, <code>call_tool</code>, <code>read_resource</code>, and every other operation. Transforms can intercept any of these, which means you can inject behavior at any point in the pipeline.</p> <p>You might be wondering: what about middleware? FastMCP still has middleware, and it operates on <em>requests</em>—intercepting tool calls, resource reads, and other operations as they execute. In FastMCP 2, some users tried to use middleware to dynamically modify tools or inject new components. It sort of worked, but it was unpredictable, hard to compose with other systems like auth and visibility, and operated at the server level which made it difficult to address subsets of components. Transforms are the clean answer: they're designed for component-level modification, they compose naturally, and they integrate with the provider system. Middleware is still there for what it's good at—authentication, logging, rate limiting, and other cross-cutting concerns at the request level. There's some gray area, but the guideline is: transforms for shaping <em>what components exist</em>, middleware for handling <em>how requests execute</em>.</p> <p>What follows is a tour of the providers and transforms that ship with FastMCP 3. Think of them less as "features" and more as building blocks—the primitives you combine to build whatever your application needs.</p> <h2>Providers</h2> <p>Providers answer the question: where do your components come from?</p> <h3>Custom Providers</h3> <p>You can write your own provider by subclassing <code>Provider</code>:</p> <pre><code>from fastmcp.server.providers import Provider class DatabaseProvider(Provider): async def list_tools(self) -&gt; Sequence[Tool]: # Query database for available tools rows = await db.fetch("SELECT * FROM tools") return [Tool(name=row['name'], description=row['description']) for row in rows] async def get_tool(self, name: str) -&gt; Tool | None: row = await db.fetchrow("SELECT * FROM tools WHERE name = ?", name) if row: return Tool(name=row['name'], description=row['description']) return None # Attach to server mcp = FastMCP("Database Server", providers=[DatabaseProvider()]) </code></pre> <p>This pattern is powerful: need tools from a REST API? Write an APIProvider. Need tools from a Kubernetes cluster? Write a KubeProvider. The provider pattern is your extension point.</p> <p><a href="https://gofastmcp.com/servers/providers/custom">Learn more in the docs →</a></p> <h3>Built-In Providers</h3> <p>FastMCP ships with providers for the most common patterns.</p> <h4>LocalProvider</h4> <p>This is the classic FastMCP experience. You define a function, decorate it, and it becomes a component. What's new in v3 is that LocalProvider is now explicit and reusable—you can attach the same provider to multiple servers.</p> <p><a href="https://gofastmcp.com/servers/providers/local">Learn more in the docs →</a></p> <pre><code>from fastmcp.server.providers import LocalProvider provider = LocalProvider() @provider.tool def greet(name: str) -&gt; str: return f"Hello, {name}!" # Attach to multiple servers server1 = FastMCP("Server1", providers=[provider]) server2 = FastMCP("Server2", providers=[provider]) </code></pre> <h4>FileSystemProvider</h4> <p>This is a fundamentally different way to organize MCP servers. Instead of importing a server instance and decorating functions, you write self-contained tool files:</p> <pre><code># mcp/tools/greet.py from fastmcp.tools import tool @tool def greet(name: str) -&gt; str: """Greet someone by name.""" return f"Hello, {name}!" </code></pre> <p>Then point the provider at the directory:</p> <pre><code>from fastmcp import FastMCP from fastmcp.server.providers import FileSystemProvider mcp = FastMCP("server", providers=[FileSystemProvider("mcp/")]) </code></pre> <p>The problem it solves: traditional servers require coordination between files—either tool files import the server (creating coupling) or the server imports all tool modules (creating a registry bottleneck). FileSystemProvider removes this coupling entirely.</p> <p>With <code>reload=True</code>, the provider re-scans on every request—changes take effect immediately without restarting the server. This is transformative for development.</p> <p><a href="https://gofastmcp.com/servers/providers/filesystem">Learn more in the docs →</a></p> <h4>SkillsProvider</h4> <p>Skills are the instruction files that Claude Code, Cursor, and Copilot use to learn new capabilities. SkillsProvider exposes these as MCP resources, which means any MCP client can discover and download skills from your server.</p> <pre><code>from pathlib import Path from fastmcp import FastMCP from fastmcp.server.providers.skills import SkillsDirectoryProvider mcp = FastMCP("Skills Server") mcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / ".claude" / "skills")) </code></pre> <p>Each subdirectory with a <code>SKILL.md</code> file becomes a discoverable skill. Clients see:</p> <ul> <li><code>skill://{name}/SKILL.md</code> - Main instruction file</li> <li><code>skill://{name}/_manifest</code> - JSON listing of all files with sizes and hashes</li> <li><code>skill://{name}/{path}</code> - Supporting files</li> </ul> <p>We also provide vendor-specific providers with locked default paths: <code>ClaudeSkillsProvider</code>, <code>CursorSkillsProvider</code>, <code>VSCodeSkillsProvider</code>, <code>CodexSkillsProvider</code>, and more.</p> <p>The FastMCP client can automatically sync skills from servers to your local filesystem, making it easy to distribute skills across your organization.</p> <p><a href="https://gofastmcp.com/servers/providers/skills">Learn more in the docs →</a></p> <h4>OpenAPIProvider</h4> <p>OpenAPI-to-MCP conversion was one of FastMCP 2's most popular features. In v3, we've restructured it as a provider, which means it now composes with everything else in the system.</p> <pre><code>from fastmcp.server.providers.openapi import OpenAPIProvider import httpx client = httpx.AsyncClient(base_url="https://api.example.com") provider = OpenAPIProvider(openapi_spec=spec, client=client) mcp = FastMCP("API Server", providers=[provider]) </code></pre> <p>All endpoints become tools by default. When paired with ToolTransform (covered below), you can rename auto-generated tools, improve descriptions, and curate the output for your agent—finally making OpenAPI conversion a tool for building <em>good</em> context rather than blindly accumulating more of it.</p> <p><a href="https://gofastmcp.com/integrations/openapi">Learn more in the docs →</a></p> <h4>ProxyProvider</h4> <p>ProxyProvider sources components from a remote MCP server. This is what powers <code>create_proxy()</code>: you connect to any MCP server and expose its components as if they were local.</p> <pre><code>from fastmcp.server import create_proxy # Create proxy to remote server server = create_proxy("http://remote-server/mcp") </code></pre> <p><a href="https://gofastmcp.com/servers/providers/proxy">Learn more in the docs →</a></p> <h4>FastMCPProvider</h4> <p>FastMCPProvider sources components from another FastMCP server instance. This is what powers <code>mount()</code>: compose servers together while keeping their middleware chains intact.</p> <pre><code>from fastmcp import FastMCP main = FastMCP("Main") sub = FastMCP("Sub") @sub.tool def greet(name: str) -&gt; str: return f"Hello, {name}!" # Mount with namespace - greet becomes "sub_greet" main.mount(sub, prefix="sub") </code></pre> <p>Under the hood, this creates a FastMCPProvider with a Namespace transform—the same primitives, with a cleaner API.</p> <p><a href="https://gofastmcp.com/servers/providers/mounting">Learn more in the docs →</a></p> <h2>Transforms</h2> <p>Transforms modify components as they flow from providers to clients. They operate on two types of methods: <strong>list</strong> operations (like <code>list_tools</code>) receive the full sequence of components and return a transformed sequence; <strong>get</strong> operations (like <code>get_tool</code>) use a middleware pattern with <code>call_next</code> to chain lookups. Transforms can be stacked, and each one processes the output of the previous.</p> <p>Transforms apply at two levels:</p> <ul> <li><strong>Provider-level</strong>: <code>provider.add_transform()</code> - affects only that provider's components</li> <li><strong>Server-level</strong>: <code>server.add_transform()</code> - affects all components from all providers</li> </ul> <h3>Built-In Transforms</h3> <h4>Namespace</h4> <p>Namespace adds prefixes to component names (<code>tool</code> → <code>api_tool</code>) and path segments to URIs (<code>data://x</code> → <code>data://api/x</code>). Essential for avoiding collisions when composing servers.</p> <pre><code>from fastmcp.server.transforms import Namespace provider.add_transform(Namespace("api")) </code></pre> <p><a href="https://gofastmcp.com/servers/transforms/namespace">Learn more in the docs →</a></p> <h4>ToolTransform</h4> <p>ToolTransform lets you reshape tools entirely: rename them, rewrite descriptions, modify argument names and schemas, add tags. This is especially powerful when you don't control the tools you're serving—if you're using OpenAPIProvider or proxying a third-party server, ToolTransform lets you optimize those auto-generated tools for your agent.</p> <pre><code>from fastmcp.server.transforms import ToolTransform from fastmcp.tools.tool_transform import ToolTransformConfig provider.add_transform(ToolTransform({ "verbose_auto_generated_name": ToolTransformConfig( name="short_name", description="A better description for the agent", tags={"category"}, ), })) </code></pre> <p><a href="https://gofastmcp.com/servers/transforms/tool-transformation">Learn more in the docs →</a></p> <h4>VersionFilter</h4> <p>VersionFilter exposes only components within a version range, letting you run v1 and v2 servers from the same codebase. See <a href="#component-versioning">Component Versioning</a> for how to define versions on your components.</p> <pre><code>from fastmcp.server.transforms import VersionFilter # Create servers that share the provider with different filters api_v1 = FastMCP("API v1", providers=[components]) api_v1.add_transform(VersionFilter(version_lt="2.0")) api_v2 = FastMCP("API v2", providers=[components]) api_v2.add_transform(VersionFilter(version_gte="2.0")) </code></pre> <h4>Visibility</h4> <p>The Visibility transform controls which components are exposed by tag, name, or version. This is what powers the <code>enable()</code> and <code>disable()</code> methods on servers and providers.</p> <pre><code>mcp.disable(tags={"admin"}) # Hide admin tools mcp.disable(names={"dangerous_tool"}) # Hide by name mcp.enable(tags={"public"}, only=True) # Allowlist mode </code></pre> <p><a href="https://gofastmcp.com/servers/visibility">Learn more in the docs →</a></p> <h4>ResourcesAsTools and PromptsAsTools</h4> <p>These transforms expose resources and prompts as tools for clients that only support the tools protocol. Some MCP hosts—particularly early adopters and simpler implementations—only expose tools to agents. These transforms let your server stay rich while still working with limited clients.</p> <pre><code>from fastmcp.server.transforms import ResourcesAsTools, PromptsAsTools mcp.add_transform(ResourcesAsTools(mcp)) mcp.add_transform(PromptsAsTools(mcp)) </code></pre> <p>ResourcesAsTools generates <code>list_resources</code> and <code>read_resource</code> tools that wrap the underlying resource operations. PromptsAsTools generates <code>list_prompts</code> and <code>get_prompt</code> tools. The transforms automatically handle argument mapping and response formatting—your resources and prompts work exactly as expected, just through the tools interface.</p> <p><a href="https://gofastmcp.com/servers/transforms/resources-as-tools">Learn more in the docs →</a></p> <h3>Custom Transforms</h3> <p>You can write your own transforms by subclassing <code>Transform</code>:</p> <pre><code>from collections.abc import Sequence from fastmcp.server.transforms import Transform, GetToolNext from fastmcp.tools import Tool class TagFilter(Transform): def __init__(self, required_tags: set[str]): self.required_tags = required_tags async def list_tools(self, tools: Sequence[Tool]) -&gt; Sequence[Tool]: # list operations receive the sequence directly return [t for t in tools if t.tags &amp; self.required_tags] async def get_tool(self, name: str, call_next: GetToolNext) -&gt; Tool | None: # get operations use call_next middleware pattern tool = await call_next(name) return tool if tool and tool.tags &amp; self.required_tags else None </code></pre> <p><a href="https://gofastmcp.com/servers/transforms/transforms">Learn more in the docs →</a></p> <h2>Authorization</h2> <p>FastMCP 3 introduces per-component authorization for tools, resources, and prompts—the missing piece after OAuth support in 2.12.</p> <h3>Component-Level Auth</h3> <p>The <code>auth</code> parameter accepts a callable (or list of callables) that receives the request context and decides whether to allow it:</p> <pre><code>from fastmcp import FastMCP from fastmcp.server.auth import require_auth, require_scopes mcp = FastMCP() @mcp.tool(auth=require_auth) def protected_tool(): ... @mcp.resource("data://secret", auth=require_scopes("read")) def secret_data(): ... @mcp.prompt(auth=require_scopes("admin")) def admin_prompt(): ... </code></pre> <p>Built-in checks:</p> <ul> <li><code>require_auth</code>: Requires any valid token</li> <li><code>require_scopes(*scopes)</code>: Requires specific OAuth scopes</li> <li><code>restrict_tag(tag, scopes)</code>: Requires scopes only for tagged components</li> </ul> <h3>Server-Wide Auth</h3> <p>Apply authorization to all components via AuthMiddleware:</p> <pre><code>from fastmcp.server.middleware import AuthMiddleware from fastmcp.server.auth import require_auth, restrict_tag # Require auth for all components mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)]) # Tag-based restrictions mcp = FastMCP(middleware=[ AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"])) ]) </code></pre> <h3>Custom Auth Checks</h3> <p>Custom checks receive <code>AuthContext</code> with <code>token</code> and <code>component</code>:</p> <pre><code>def custom_check(ctx: AuthContext) -&gt; bool: return ctx.token is not None and "admin" in ctx.token.scopes </code></pre> <p>Note: STDIO transport bypasses all auth checks (no OAuth concept).</p> <p><a href="https://gofastmcp.com/servers/authorization">Learn more in the docs →</a></p> <h3>CIMD</h3> <p>CIMD (Client ID Metadata Document) is the successor to Dynamic Client Registration. Instead of clients registering via a POST endpoint, they provide an HTTPS URL pointing to their metadata document. The server fetches and validates it, which is more secure and enables better client verification. Shipped in <a href="/blog/fastmcp-3-beta-2#cimd-client-authentication-without-dcr">beta 2</a>.</p> <p><a href="https://gofastmcp.com/clients/auth/cimd">Learn more in the docs →</a></p> <h2>Component Versioning</h2> <p>You can now register multiple versions of the same component. FastMCP automatically exposes the highest version to clients while preserving older versions for compatibility.</p> <h3>Declaring Versions</h3> <pre><code>@mcp.tool(version="1.0") def add(x: int, y: int) -&gt; int: return x + y @mcp.tool(version="2.0") def add(x: int, y: int, z: int = 0) -&gt; int: return x + y + z # Only v2.0 is exposed via list_tools() # Calling "add" invokes the v2.0 implementation </code></pre> <p>Version comparison uses PEP 440 semantic versioning (1.10 &gt; 1.9 &gt; 1.2). The <code>v</code> prefix is normalized (<code>v1.0</code> equals <code>1.0</code>).</p> <h3>Version Metadata</h3> <p>When listing components, FastMCP exposes all available versions in the <code>meta</code> field:</p> <pre><code>tools = await client.list_tools() # Each tool's meta includes: # - meta["fastmcp"]["version"]: the version of this component ("2.0") # - meta["fastmcp"]["versions"]: all available versions ["2.0", "1.0"] </code></pre> <h3>Calling Specific Versions</h3> <p>The FastMCP client supports direct version selection:</p> <pre><code>from fastmcp import Client async with Client(server) as client: # Call the latest version (default) result = await client.call_tool("add", {"x": 1, "y": 2}) # Call a specific version result = await client.call_tool("add", {"x": 1, "y": 2}, version="1.0") </code></pre> <p>For generic MCP clients that don't support the version parameter, pass version via <code>_meta</code> in arguments.</p> <p><a href="https://gofastmcp.com/servers/versioning">Learn more in the docs →</a></p> <pre><code>{ "x": 1, "y": 2, "_meta": { "fastmcp": { "version": "1.0" } } } </code></pre> <h2>Session-Scoped State</h2> <p>State now persists across tool calls within a session, not just within a single request.</p> <pre><code>@mcp.tool async def increment_counter(ctx: Context) -&gt; int: count = await ctx.get_state("counter") or 0 await ctx.set_state("counter", count + 1) return count + 1 </code></pre> <p>State is automatically keyed by session ID, ensuring isolation between different clients.</p> <p><strong>Key changes from v2:</strong></p> <ul> <li>Methods are now async: <code>await ctx.get_state()</code>, <code>await ctx.set_state()</code>, <code>await ctx.delete_state()</code></li> <li>State expires after 1 day (TTL) to prevent unbounded growth</li> </ul> <p><strong>Distributed backends:</strong></p> <p>The implementation uses <a href="https://github.com/strawgate/py-key-value">pykeyvalue</a> (maintained by FastMCP maintainer Bill Easton) for pluggable storage:</p> <pre><code>from key_value.aio.stores.redis import RedisStore # Use Redis for distributed deployments mcp = FastMCP("server", session_state_store=RedisStore(...)) </code></pre> <p><strong>Stateless HTTP:</strong></p> <p>For stateless HTTP deployments where there's no persistent connection, FastMCP respects the <code>mcp-session-id</code> header that most clients send. If you've configured a storage backend, we'll create a virtual session for you.</p> <p><a href="https://gofastmcp.com/servers/storage-backends">Learn more in the docs →</a></p> <h2>Visibility System</h2> <p>Components can be enabled or disabled using the visibility system. Each <code>enable()</code> or <code>disable()</code> call adds a Visibility transform that marks components.</p> <pre><code>mcp = FastMCP("Server") # Disable by name mcp.disable(names={"dangerous_tool"}, components=["tool"]) # Disable by tag mcp.disable(tags={"admin"}) # Allowlist mode - only show components with these tags mcp.enable(tags={"public"}, only=True) # Enable overrides earlier disable (later transform wins) mcp.disable(tags={"internal"}) mcp.enable(names={"safe_tool"}) # safe_tool is visible despite internal tag </code></pre> <p><strong>Blocklist vs Allowlist:</strong></p> <ul> <li><strong>Blocklist mode</strong> (default): All components visible except explicitly disabled</li> <li><strong>Allowlist mode</strong> (<code>only=True</code>): Only explicitly enabled components visible</li> </ul> <h3>Per-Session Visibility</h3> <p>Server-level visibility changes affect all connected clients. For per-session control, use <code>Context</code> methods:</p> <pre><code>@mcp.tool(tags={"premium"}) def premium_analysis(data: str) -&gt; str: return f"Premium analysis of: {data}" @mcp.tool async def unlock_premium(ctx: Context) -&gt; str: """Unlock premium features for this session only.""" await ctx.enable_components(tags={"premium"}) return "Premium features unlocked" @mcp.tool async def reset_features(ctx: Context) -&gt; str: """Reset to default feature set.""" await ctx.reset_visibility() return "Features reset to defaults" # Globally disabled - sessions unlock individually mcp.disable(tags={"premium"}) </code></pre> <p>Session visibility methods:</p> <ul> <li><code>await ctx.enable_components(...)</code>: Enable components for this session</li> <li><code>await ctx.disable_components(...)</code>: Disable components for this session</li> <li><code>await ctx.reset_visibility()</code>: Clear session rules, return to global defaults</li> </ul> <p>FastMCP automatically sends <code>ToolListChangedNotification</code> (and resource/prompt equivalents) to affected sessions when visibility changes.</p> <p><a href="https://gofastmcp.com/server/visibility">Learn more in the docs →</a></p> <h2>Production Features</h2> <h3>OpenTelemetry Tracing</h3> <p>FastMCP 3 has native OpenTelemetry instrumentation. Drop in your OTEL configuration, and every tool call, resource read, and prompt render is traced with standardized attributes.</p> <pre><code>from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider) # Use fastmcp normally - spans export to your configured backend </code></pre> <p>Server spans include: component key, provider type, session ID, auth context. Client spans wrap outgoing calls with W3C trace context propagation.</p> <p><a href="https://gofastmcp.com/servers/telemetry">Learn more in the docs →</a></p> <h3>Background Tasks (SEP-1686)</h3> <p>MCP has a spec extension (SEP-1686) for long-running background tasks. FastMCP implements this via Docket integration—you get persistent task queues backed by SQLite or Postgres, with the ability to scale workers horizontally.</p> <pre><code>from fastmcp.server.tasks import TaskConfig @mcp.tool(task=TaskConfig(mode="required")) async def long_running_task(): # Must be executed as background task ... @mcp.tool(task=TaskConfig(mode="optional")) async def flexible_task(): # Supports both sync and task execution ... @mcp.tool(task=True) # Shorthand for mode="optional" async def simple_task(): ... </code></pre> <p>Task modes:</p> <ul> <li><code>"forbidden"</code>: Does not support task execution (default)</li> <li><code>"optional"</code>: Supports both synchronous and task execution</li> <li><code>"required"</code>: Must be executed as background task</li> </ul> <p>Install with <code>fastmcp[tasks]</code> for Docket integration.</p> <p><a href="https://gofastmcp.com/servers/tasks">Learn more in the docs →</a></p> <h3>Tool Timeouts</h3> <p>Tools can limit foreground execution time:</p> <pre><code>@mcp.tool(timeout=30.0) async def fetch_data(url: str) -&gt; dict: """Fetch with 30-second timeout.""" ... </code></pre> <p>When exceeded, clients receive MCP error code <code>-32000</code>. Both sync and async tools are supported. Note: timeouts don't apply to background tasks—those run in Docket's task queue with their own lifecycle management.</p> <p><a href="https://gofastmcp.com/servers/tools">Learn more in the docs →</a></p> <h3>Pagination</h3> <p>For servers with many components, enable pagination:</p> <pre><code>server = FastMCP("ComponentRegistry", list_page_size=50) </code></pre> <p>When <code>list_page_size</code> is set, list operations paginate responses with <code>nextCursor</code> for subsequent pages. The FastMCP Client fetches all pages automatically—<code>list_tools()</code> returns the complete list. For manual pagination:</p> <p><a href="https://gofastmcp.com/servers/pagination">Learn more in the docs →</a></p> <pre><code>async with Client(server) as client: result = await client.list_tools_mcp() while result.nextCursor: result = await client.list_tools_mcp(cursor=result.nextCursor) </code></pre> <h3>PingMiddleware</h3> <p>Keep long-lived connections alive with periodic pings:</p> <pre><code>from fastmcp.server.middleware import PingMiddleware mcp = FastMCP("server") mcp.add_middleware(PingMiddleware(interval_ms=5000)) </code></pre> <p><a href="https://gofastmcp.com/servers/middleware">Learn more in the docs →</a></p> <h2>Developer Experience</h2> <h3>Decorators Return Functions</h3> <p>By popular demand (and by "popular demand" I mean "relentless GitHub issues"), your decorated functions now stay callable, like they do in Flask, FastAPI, and Typer:</p> <pre><code>@mcp.tool def greet(name: str) -&gt; str: return f"Hello, {name}!" # greet is still your function - call it directly greet("World") # "Hello, World!" </code></pre> <p>This makes testing straightforward: just call the function. For v2 compatibility, set <code>FASTMCP_DECORATOR_MODE=object</code>.</p> <p><a href="https://gofastmcp.com/getting-started/defining-tools">Learn more in the docs →</a></p> <h3>Hot Reload</h3> <p><code>fastmcp run --reload</code> watches your files and reloads automatically:</p> <pre><code># Watch for changes and restart fastmcp run server.py --reload # Watch specific directories fastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib </code></pre> <p>The <code>fastmcp dev</code> command is a shorthand that includes <code>--reload</code> by default.</p> <p><a href="https://gofastmcp.com/patterns/cli">Learn more in the docs →</a></p> <h3>Automatic Threadpool</h3> <p>Synchronous tools, resources, and prompts now automatically run in a threadpool:</p> <pre><code>import time @mcp.tool def slow_tool(): time.sleep(10) # No longer blocks other requests return "done" </code></pre> <p>Three concurrent calls now execute in parallel (~10s) rather than sequentially (30s).</p> <p><a href="https://gofastmcp.com/servers/tools">Learn more in the docs →</a></p> <h3>Composable Lifespans</h3> <p>Lifespans can be combined with the <code>|</code> operator for modular setup/teardown:</p> <pre><code>from fastmcp import FastMCP from fastmcp.server.lifespan import lifespan @lifespan async def db_lifespan(server): db = await connect_db() try: yield {"db": db} finally: await db.close() @lifespan async def cache_lifespan(server): cache = await connect_cache() try: yield {"cache": cache} finally: await cache.close() mcp = FastMCP("server", lifespan=db_lifespan | cache_lifespan) </code></pre> <p>Both enter in order and exit in reverse (LIFO). Context dicts are merged.</p> <p><a href="https://gofastmcp.com/servers/lifespan">Learn more in the docs →</a></p> <h3>Rich Result Classes</h3> <p>New result classes provide explicit control over component responses:</p> <p><strong>ToolResult:</strong></p> <pre><code>from fastmcp.tools import ToolResult @mcp.tool def process(data: str) -&gt; ToolResult: return ToolResult( content=[TextContent(type="text", text="Done")], structured_content={"status": "success", "count": 42}, meta={"processing_time_ms": 150} ) </code></pre> <p><strong>ResourceResult:</strong></p> <pre><code>from fastmcp.resources import ResourceResult, ResourceContent @mcp.resource("data://items") def get_items() -&gt; ResourceResult: return ResourceResult( contents=[ ResourceContent({"key": "value"}), ResourceContent(b"binary data"), ], meta={"count": 2} ) </code></pre> <p><strong>PromptResult:</strong></p> <pre><code>from fastmcp.prompts import PromptResult, Message @mcp.prompt def conversation() -&gt; PromptResult: return PromptResult( messages=[ Message("What's the weather?"), Message("It's sunny today.", role="assistant"), ], meta={"generated_at": "2024-01-01"} ) </code></pre> <p><a href="https://gofastmcp.com/servers/tools">Learn more in the docs →</a></p> <h3>Context.transport Property</h3> <p>Tools can detect which transport is active:</p> <pre><code>@mcp.tool def my_tool(ctx: Context) -&gt; str: if ctx.transport == "stdio": return "short response" return "detailed response with more context" </code></pre> <p>Returns <code>"stdio"</code>, <code>"sse"</code>, or <code>"streamable-http"</code>.</p> <p><a href="https://gofastmcp.com/servers/context">Learn more in the docs →</a></p> <h2>Upgrading</h2> <p>The vast majority of users can upgrade with no modifications. The breaking changes are documented in the <a href="https://gofastmcp.com/development/upgrade-guide">upgrade guide</a>, but the main ones are:</p> <ul> <li><strong>Decorators return functions</strong> (set <code>FASTMCP_DECORATOR_MODE=object</code> for v2 behavior)</li> <li><strong>State methods are async</strong> (<code>await ctx.get_state()</code> instead of <code>ctx.get_state()</code>)</li> <li><strong>Auth providers require explicit configuration</strong> (no more auto-loading from env vars)</li> <li><strong><code>enabled</code> parameter removed from components</strong> (use the visibility system instead: <code>mcp.enable()</code> / <code>mcp.disable()</code>)</li> </ul> <hr /> <ul> <li><strong>Upgrade:</strong> <code>pip install fastmcp==3.0.0b2</code></li> <li><strong>Docs:</strong> <a href="https://gofastmcp.com">Read the new documentation</a></li> <li><strong>GitHub:</strong> <a href="https://github.com/jlowin/fastmcp">Star the repo</a></li> </ul> <p>Happy (context) engineering!</p> Introducing FastMCP 3.0 🚀https://jlowin.dev/blog/fastmcp-3/https://jlowin.dev/blog/fastmcp-3/Move fast and make things.Tue, 20 Jan 2026 12:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>I have a confession to make.</p> <p>FastMCP 2.0 hides a dark secret. For the last year, we have been scrambling. We were riding the adoption curve of one of the fastest-growing technologies on the planet, trying to keep up with a spec that seemed to change every week.</p> <p>On the one hand, it worked. <strong>FastMCP 1.0</strong> proved the concept so well that Anthropic made it the foundation of the official MCP SDK. <strong>FastMCP 2.0</strong> introduced the features necessary to build a real server ecosystem, coinciding with the massive MCP hype wave. The community responded: Today, FastMCP is downloaded <strong>a million times a day</strong>, and some version of it powers 70% of all MCP servers.</p> <p>But as someone who cares deeply about framework design, the way v2 evolved was frustrating. It was <em>reactive</em>. We were constantly bolting on new infrastructure to match the present, hacking in new features just to make sure you didn't have to build them yourself.</p> <p>Over the last year, something shifted. We had enough data, from millions of downloads and countless conversations with teams building real servers, to see the patterns underneath all the ad-hoc features. We could finally see what a "designed" framework would look like.</p> <p><strong>FastMCP 3.0 is that framework.</strong></p> <p>It is the platform MCP deserves in 2026, built to be as <strong>durable</strong> as it is <strong>future-proof</strong>.</p> <p>We are moving beyond simple "tool servers." We are entering the era of <strong>Context Applications</strong>—rich, adaptive systems that manage the information flow to agents.</p> <p>The real challenge was never implementing the protocol. It's delivering the right information at the right time. FastMCP 3 is built for that:</p> <ul> <li><strong>Source</strong> components from anywhere.</li> <li><strong>Compose</strong> and transform them freely.</li> <li><strong>Personalize</strong> what each user sees.</li> <li><strong>Track</strong> state across sessions.</li> <li><strong>Control</strong> access at every level.</li> <li><strong>Run</strong> long operations in the background.</li> <li><strong>Version</strong> your APIs.</li> <li><strong>Observe</strong> everything.</li> </ul> <p>It's time to move fast and make things.</p> <p>&lt;Callout color="green"&gt;</p> <h3>🏁 Get Started</h3> <p>FastMCP 3.0.0 beta 2 is <a href="https://gofastmcp.com/getting-started/installation">available now</a>.</p> <p>For a deeper dive into all the new features, read the <a href="/blog/fastmcp-3-whats-new">What's New in FastMCP 3.0</a> post and the <a href="/blog/fastmcp-3-beta-2">Beta 2 announcement</a>. &lt;/Callout&gt;</p> <h2>The Architecture</h2> <p>FastMCP 2 was a collection of features. FastMCP 3 is a system built on three fundamental primitives. If you understand these, you understand the entire framework.</p> <ol> <li><strong>Components</strong> define the logic.</li> <li><strong>Providers</strong> source the components.</li> <li><strong>Transforms</strong> shape the components.</li> </ol> <p>A <strong>Component</strong> is the atom of MCP—specifically, a <strong>Tool</strong>, <strong>Resource</strong>, or <strong>Prompt</strong>. While they often wrap Python functions or data sources to define their business logic, the Component itself is the standardized interface that the model interacts with.</p> <p>A <strong>Provider</strong> answers the question: <em>"Where do the components come from?"</em> They can come from Python decorators, a directory of files, an OpenAPI spec, a remote MCP server, or pretty much anything else. In fact, a FastMCP server is itself just a Provider that happens to speak the MCP protocol.</p> <p>A <strong>Transform</strong> functions as middleware for Providers. It allows you to modify the behavior of a Provider without touching its code. This decouples the author from the consumer: Person A can source the tools (via a Provider), while Person B adapts them to their specific environment (via a Transform)—renaming them, adding namespaces to prevent collisions, filtering versions, or applying security rules.</p> <p>The real power lies in the <strong>composition</strong> of these primitives.</p> <p>In v2, "mounting" a sub-server was a massive, specialized subsystem. In v3 it's just a <strong>Provider</strong> (sourcing the components) plus a <strong>Transform</strong> (adding a namespace prefix).</p> <p>Proxying a remote server? That's a <strong>Provider</strong> backed by a FastMCP client.</p> <p>Hiding developer tools from read-only users? That's a <strong>Transform</strong> applied to a specific session.</p> <p>This architecture means features that used to require massive amounts of glue code now fall out naturally from the design. It allows us to ship a massive amount of new functionality without breaking the foundation.</p> <h2>Sourcing Context: Providers</h2> <p>Because the architecture is decoupled, we can now source components from anywhere.</p> <h3>LocalProvider</h3> <p>This workhorse powers the classic FastMCP experience you know and love. You define a function, decorate it with <code>@tool</code>, and it becomes a component. It is simple, explicit, and remains the best way to get started. But what if your tools aren't local?</p> <h3>FileSystemProvider</h3> <p>This is a fundamentally different way to organize MCP servers. Instead of importing a server instance and decorating functions, you point the provider at a directory. It scans the files, finds the components, and builds your interface. With <code>reload=True</code>, it watches those files and updates the server instantly on any change.</p> <h3>SkillsProvider</h3> <p>Skills are having a moment. Claude Code, Cursor, Copilot—they all learn new capabilities from instruction files. SkillsProvider exposes these as MCP resources, which means any MCP client can discover and download skills from your server. We're delivering skills over MCP. It's a small example of what happens when "where do components come from?" becomes an open question: <a href="https://x.com/aaazzam">someone</a> had a weird idea, wrote a provider, and now it's a capability.</p> <h3>OpenAPIProvider</h3> <p>This feature was so popular in FastMCP 2 that people stopped designing servers and started regurgitating REST APIs, <a href="https://www.jlowin.dev/blog/stop-converting-rest-apis-to-mcp">forcing me to write a blog post asking you to stop</a>. But we know: it's useful. In FastMCP 3, OpenAPI returns as a provider. It is available for responsible use, and when paired with <strong>ToolTransforms</strong> (to rename and curate the output), it finally becomes a tool for building <em>good</em> context rather than blindly accumulating <em>more</em> of it.</p> <h2>Production Realities</h2> <p>FastMCP 2 was great for scripts. FastMCP 3 is built for systems that need to survive in production.</p> <h3>Component Versioning</h3> <p>This was a massive request. You can now serve multiple versions of a tool side-by-side using the <code>@tool(version="1.0")</code> parameter. FastMCP automatically exposes the highest version to clients, while preserving older versions for legacy compatibility. You can even use a <strong>VersionFilter</strong> transform to run a "v1 Server" and a "v2 Server" from the exact same codebase.</p> <h3>Authorization &amp; Security</h3> <p>We introduced OAuth in v2, but v3 gives you granular control. You can attach authorization logic to individual components using the <code>auth</code> parameter. You can also apply <strong>AuthMiddleware</strong> to gate entire groups of components (e.g., by tag) for defense-in-depth.</p> <h3>Native OpenTelemetry</h3> <p>Observability is no longer an afterthought. FastMCP 3 has native OpenTelemetry instrumentation. Drop in your OTEL configuration, and every tool call, resource read, and prompt render is traced with standardized attributes. You can finally see exactly where your latency is coming from.</p> <h3>Background Tasks</h3> <p>We've integrated support for SEP-1686, allowing tools to kick off long-running background tasks via Docket integration. This prevents tool timeouts on heavy workloads while keeping the agent responsive.</p> <h2>Developer Joy</h2> <p>We heard you. You wanted a framework that felt less like a hacked-together library and more like a modern Python toolchain.</p> <ul> <li><strong>Hot Reload:</strong> <code>fastmcp dev server.py</code> watches your files and reloads instantly. No more kill-restart cycles.</li> <li><strong>Callable Functions:</strong> In v2, decorators turned your functions into objects. In v3, your functions stay functions. You can import them, call them, and unit test them just like normal Python code.</li> <li><strong>Sync that Works:</strong> Synchronous tools are now automatically dispatched to a threadpool, meaning a slow calculation won't block your server's event loop.</li> </ul> <h2>Playbooks</h2> <p>I want to close by showing you why this architecture actually matters.</p> <p>A common problem in MCP is "context crowding." If you dump 500 tools into a context window, the model gets confused. You want <strong>progressive disclosure</strong>: start with a few tools, and reveal more based on the user's role or the conversation state.</p> <p>In FastMCP 3, we don't need a special "Progressive Disclosure" feature[^pd]. We just compose the primitives we've already built:</p> <ol> <li><strong>Providers</strong> to source the hidden tools.</li> <li><strong>Visibility</strong> to hide them by default.</li> <li><strong>Auth</strong> to act as the gatekeeper.</li> <li><strong>Session State</strong> to remember who has unlocked what.</li> </ol> <p>Here is what that looks like. We mount a directory of admin tools, hide them from the world, and then provide a secure, authenticated tool that unlocks them <em>only for the current session</em>.</p> <pre><code>from fastmcp import FastMCP, Context from fastmcp.server.auth import require_scopes from fastmcp.server.providers import FileSystemProvider mcp = FastMCP("Enterprise Server") # 1. Source admin tools from a file system admin_provider = FileSystemProvider("./admin_tools") mcp.mount(admin_provider) # 2. Hide them by default using the Visibility system mcp.disable(tags={"admin"}) # 3. Create a gatekeeper tool with Authorization @mcp.tool(auth=require_scopes("super-user")) async def unlock_admin_mode(ctx: Context): """Unlock administrative tools for this session.""" # 4. Modify Session State to reveal the hidden tools await ctx.enable_components(tags={"admin"}) return "Admin mode unlocked. New tools are available." </code></pre> <p>The agent connects, sees a safe environment, authenticates, and the server <em>evolves</em> to match the new trust level.</p> <p>This composition creates a new primitive entirely. When you chain these stateful unlocks together—revealing context A, which unlocks context B—you get what we call <strong>playbooks</strong>. Playbooks are a way to build dynamic MCP-native workflows. More on them soon!</p> <p>This is the future of Context Applications. Static lists of API wrappers are being replaced by dynamic systems that actively guide the agent through a process.</p> <h2>The Future</h2> <p>We know that as capabilities grow, context windows get crowded. The hundred tools that make your server powerful are the same hundred tools that overwhelm your agent.</p> <p>Our next wave of features is focused on <strong>context optimization</strong>: search transforms, curator agents, and deeper skills integration. The architecture of FastMCP 3 is specifically designed to support these patterns.</p> <p>Because what you <em>don't</em> show the agent matters just as much as what you do.</p> <p>Today, organizations with a competitive advantage don't have access to smarter AI. They have access to smarter context. FastMCP 3 is the fastest to build it.</p> <p>It's available in beta <a href="https://gofastmcp.com/getting-started/installation">today</a>.</p> <p>Happy (context) engineering!</p> <hr /> <p>&lt;Callout color="gray"&gt;</p> <h3>About This Beta</h3> <p>FastMCP is an extremely widely used framework. While 3.0 introduces almost no breaking changes, we want to make sure that users aren't caught off guard. Therefore, the beta period will last a few weeks to allow for feedback and testing.</p> <p><strong>Install:</strong> <code>pip install fastmcp==3.0.0b2</code></p> <ul> <li><strong>Beta 2 Announcement:</strong> <a href="/blog/fastmcp-3-beta-2">FastMCP 3.0 Beta 2: The Toolkit</a></li> <li><strong>Upgrade Guide:</strong> <a href="https://gofastmcp.com/development/upgrade-guide">gofastmcp.com/development/upgrade-guide</a></li> <li><strong>Full Documentation:</strong> <a href="https://gofastmcp.com">gofastmcp.com</a></li> <li><strong>GitHub:</strong> <a href="https://github.com/jlowin/fastmcp">github.com/jlowin/fastmcp</a> &lt;/Callout&gt;</li> </ul> <p>[^pd]: Though of course we'll have an amazing DX for it as patterns emerge.</p> Welcoming Bill Easton to the FastMCP Teamhttps://jlowin.dev/blog/fastmcp-bill-easton/https://jlowin.dev/blog/fastmcp-bill-easton/Merge to mainTue, 02 Sep 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>FastMCP has quickly become the most popular framework for building MCP servers, and its momentum has been incredible to watch. To sustain that growth and continue pushing the ecosystem forward, I'm thrilled to announce the appointment of FastMCP's first external maintainer: <strong><a href="https://linkedin.com/in/williamseaston">Bill Easton</a></strong>.</p> <p>Bill is the Director of Product Management for Observability at Elastic, and folks active on the FastMCP repository will certainly recognize him by his GitHub handle, <strong><a href="https://github.com/strawgate">@strawgate</a></strong>. From the very beginning, he has been one of the most prolific and thoughtful contributors to the project.</p> <p>Bill showed up months ago (long before MCP was cool) and immediately began insisting on the need for utilities to manipulate, transform, and compose MCP servers into more usable forms. He has consistently impressed me, not just with the care that goes into his work, but with how prescient he's been in looking around corners to anticipate the use cases that are now considered obvious in the MCP ecosystem.</p> <p>Many of Bill's contributions are the type we prize most in the open-source world: critical internal optimizations and fixes that ensure your server just works, even if you're not aware you're using them. They're the kind of thoughtful, careful improvements that make the difference between a demo and production-ready software.</p> <p>Bill is also the driving force behind our entire suite of <strong><a href="https://gofastmcp.com/patterns/tool-transformation">tool transformation features</a></strong>. These features are absolutely critical, especially for anyone generating MCP servers from large OpenAPI specs, and we expect them to become even more important as MCP servers get more complex.</p> <p>Here's the backstory: when we first launched OpenAPI conversion, it seemed like magic. Feed in your REST API spec, get out an MCP server. But the reality was messier. Those auto-generated servers were often so unusable that I wrote a whole blog post <a href="https://www.jlowin.dev/blog/stop-converting-rest-apis-to-mcp">urging people to stop using the feature</a>. The servers worked technically, but they were poisoning agents with hundreds of atomic, context-free operations.</p> <p>Bill saw this problem differently. Instead of abandoning OpenAPI conversion, he built a sophisticated transformation layer that lets you reshape, combine, and filter tools intelligently. His contributions turned what was an anti-pattern into a powerful, production-ready workflow. Today, some of the most sophisticated FastMCP deployments rely on these transformation features to create agent-friendly interfaces from complex REST APIs.</p> <p>What excites me most is what this represents for the project's future. Bill's appointment reflects a commitment to building a sustainable, community-driven project that can outlive any single contributor. The best open-source projects are those where leadership emerges naturally from the community, and Bill exemplifies exactly that kind of organic leadership.</p> <p>As such, as part of formalizing Bill's role, we're also introducing a more structured way for the community to engage with the project's development. Starting soon, we'll host <strong>meetings every two weeks</strong> to discuss the project. These will operate in an alternating fashion:</p> <ul> <li> <p><strong>Committer's Meeting (Bi-weekly):</strong> One session each month will be a closed meeting for maintainers to discuss and plan the project roadmap. To maintain transparency, notes and key decisions will be shared publicly.</p> </li> <li> <p><strong>Community Office Hours (Bi-weekly):</strong> The alternating session will be an open meeting for the entire community. These will be office-hours-style, with a published agenda ahead of time. While we welcome discussion, these forums won't be for debugging idiosyncratic issues that are better served by an asynchronous format like GitHub issues.</p> </li> </ul> <p>We're currently planning the first office hours session and will share details with the community soon.</p> <p>Happy engineering!</p> <p>&lt;Callout color="gray"&gt;</p> <ul> <li><strong>Contribute:</strong> Check out the code and examples on <a href="https://github.com/jlowin/fastmcp">GitHub</a></li> <li><strong>Explore:</strong> Dig into the <a href="https://gofastmcp.com/">FastMCP documentation</a></li> <li><strong>Upgrade:</strong> <code>uv add fastmcp</code> or <code>pip install fastmcp --upgrade</code> &lt;/Callout&gt;</li> </ul> MCP Proxy Servers with FastMCP 2.0https://jlowin.dev/blog/fastmcp-proxy/https://jlowin.dev/blog/fastmcp-proxy/Even AI needs a good travel adapter 🔌Wed, 23 Apr 2025 00:00:00 GMT<p>In the <a href="/blog/fastmcp-2">FastMCP 2.0 release post</a>, I highlighted how the project's focus has evolved from simply <em>creating</em> MCP servers in 1.0, to making it easier to <em>work with</em> the growing ecosystem in 2.0. MCP <a href="https://gofastmcp.com/patterns/composition">composition</a> addresses various ways of combining your MCP servers. But what about integrating with servers you don't control, like remote services, third-party tools, or servers using different transports?</p> <p>This is where <strong>proxying</strong> comes in. It's a core piece of the FastMCP 2.0 vision, enabling seamless interaction across the diverse landscape of MCP servers.</p> <h2>What is an MCP Proxy?</h2> <p>A FastMCP proxy acts as an intermediary, a kind of universal travel adapter. You run a FastMCP server instance, but instead of implementing its own logic, it forwards requests to a designated <em>backend</em> MCP server.</p> <p>Here’s the flow:</p> <ol> <li>Your client application sends a request (e.g., <code>call tool XYZ</code>) to the local FastMCP proxy.</li> <li>The proxy receives this request and forwards it to the configured backend server (which could be anywhere, using any transport).</li> <li>The backend server processes the request and sends its response back to the proxy.</li> <li>The proxy relays this response to your original client.</li> </ol> <p><img src="./diagram.png" alt="" /></p> <p>To the client, it looks like a standard FastMCP interaction. Under the hood, the proxy handles the communication translation.</p> <p>This capability might seem simple, but it solves several practical challenges in building and using MCP-based systems:</p> <ul> <li> <p><strong>Transport Bridging</strong>: This is fundamental. Many powerful MCP servers might only be available via network transports like <code>sse</code> or <code>websocket</code>. However, local clients like Claude Desktop often expect to communicate via <code>stdio</code>. A FastMCP proxy running locally can bridge this gap, listening on <code>stdio</code> while communicating with the backend via <code>sse</code> (or vice-versa, or any other combination). This instantly makes remote or differently-transported servers accessible to local tools.</p> </li> <li> <p><strong>Interaction Simplification</strong>: Instead of managing connections to numerous backend servers with potentially different addresses and transports, applications can interact with a single, local proxy endpoint. The proxy handles the complexity of routing requests to the appropriate backend, streamlining client configuration.</p> </li> <li> <p><strong>Gateway Functionality</strong>: A proxy can serve as a controlled entry point to backend services. While base proxying forwards requests directly, the underlying <code>FastMCPProxy</code> class could be subclassed (for advanced users) to inject logic like request logging, caching, authentication checks, or even basic request/response modification, creating a more robust gateway.</p> </li> <li> <p><strong>Decoupling</strong>: Proxies decouple the client's required transport from the backend server's implementation. The backend server can change its transport or location, and only the proxy configuration needs updating, not every client application.</p> </li> </ul> <h2>Creating a Proxy</h2> <p>FastMCP makes creating a proxy straightforward using the <code>FastMCP.from_client()</code> class method. It leverages the standard <code>fastmcp.Client</code> to define the connection to the backend.</p> <pre><code>from fastmcp import FastMCP, Client # 1. Configure a client for the backend server. # This target could be anything the Client can connect to: # - Remote SSE/WebSocket URL: Client("http://api.example.com/mcp/sse") # - Local Python script: Client("path/to/backend_server.py") # - Another FastMCP instance: Client(another_mcp_instance) backend_client = Client("http://api.example.com/mcp/sse") # 2. Create the proxy server instance from the client. proxy_server = FastMCP.from_client( backend_client, name="MySmartProxy" ) # 3. Run the proxy server (defaults to stdio) if __name__ == "__main__": proxy_server.run() # You could run it on SSE instead if needed: # proxy_server.run(transport="sse", port=9001) </code></pre> <p>When you call <code>FastMCP.from_client()</code>, it doesn't discover the backend components immediately. Instead, it stores the provided <code>backend_client</code> within the <code>proxy_server</code> instance. When the proxy server later receives a request (like <code>list_tools</code> or <code>call_tool</code>), it dynamically uses the stored <code>backend_client</code> at that moment to forward the request to the backend and relay the response. This ensures the proxy always reflects the current state of the backend server. The result is a standard <code>FastMCP</code> instance, ready to run.</p> <h3>Proxies Love Composition</h3> <p>Crucially, because <code>FastMCP.from_client()</code> yields a standard <code>FastMCP</code> instance, these proxies integrate perfectly with FastMCP 2.0's composition model. You can call <code>mount()</code> or <code>import_server()</code> to compose a proxy server alongside other servers, just like any other FastMCP server.</p> <pre><code>from fastmcp import FastMCP, Client main_app = FastMCP(name="CombinedApp") @main_app.tool() def local_utility(): return "This tool runs directly in the main app." # Assume proxy_server is created as shown before proxy_server = FastMCP.from_client(...) # Mount the proxy server instance under a prefix main_app.mount("proxied_service", proxy_server) # The main_app now exposes: # - "local_utility" (its own tool) # - "proxied_service_&lt;backend_tool_name&gt;" (tools from the backend via the proxy) # - "proxied_service+&lt;backend_resource_uri&gt;" (resources from the backend via the proxy) if __name__ == "__main__": main_app.run() </code></pre> <p>This allows you to build sophisticated applications where a single FastMCP server acts as a unified interface to both local functionality and multiple remote or diverse backend MCP services.</p> <h2>Wrapping Up</h2> <p>Proxying is essential plumbing for a truly interoperable MCP ecosystem. FastMCP 2.0 makes it trivial to bridge transports, simplify client interactions, and integrate disparate MCP servers. By treating proxies as first-class FastMCP servers, we unlock flexible and powerful architectural patterns.</p> <p>If you haven't already, explore the proxying capability – it might just be the missing piece for connecting your AI workflows. Check out the <a href="https://gofastmcp.com/patterns/proxy">proxying docs</a> for further details.</p> <p>Happy Connecting! 🔌</p> Stop Vibe-Testing Your MCP Serverhttps://jlowin.dev/blog/stop-vibe-testing-mcp-servers/https://jlowin.dev/blog/stop-vibe-testing-mcp-servers/Your tests are bad and you should feel badWed, 21 May 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>If you're working with the Model Context Protocol (MCP), you're on the front lines of AI innovation. But amidst the excitement of creating intelligent agents and sophisticated AI workflows, I need to ask: <strong>how are you actually testing these critical MCP components?</strong></p> <p>Too often, the answer looks something like this: fire up an agent framework, type a few prompts into a chat window, and if the LLM <em>seems</em> to produce a reasonable output, call it a day. This, my friends, is <strong>vibe-testing.</strong></p> <p>To be fair, this isn't entirely surprising. The MCP ecosystem is young, and the developer tooling is still catching up to the rapid pace of protocol adoption. However, while vibe-testing might seem pragmatic given the tooling landscape, it's a fast track to unreliable systems, wasted tokens, and downright painful debugging sessions.</p> <p>MCP servers are the APIs that connect LLMs to the real world. And like any critical API, they demand rigorous, deterministic testing to ensure they are reliable, predictable, and robust—especially when the primary consumer is a non-deterministic LLM.</p> <p>&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;A QA engineer walks into a bar. Orders a beer. Orders 0 beers. Orders 99999999999 beers. Orders a lizard. Orders -1 beers. Orders a ueicbksjdhd.&lt;br/&gt;&lt;br/&gt;First real customer walks in and asks where the bathroom is. The bar bursts into flames, killing everyone.&lt;/p&gt;— Brenan Keller (@brenankeller) &lt;a href="https://twitter.com/brenankeller/status/1068615953989087232?ref_src=twsrc%5Etfw"&gt;November 30, 2018&lt;/a&gt;&lt;/blockquote&gt;</p> <p>This joke hits alarmingly close to home in the MCP world. Traditionally, QA engineers intentionally probe boundaries. With MCP, <em>your LLM client is a chaos agent.</em> LLMs can generate unexpected or malformed inputs, explore edge cases you never envisioned, or chain calls in ways that defy simple logic. If your MCP server isn't hardened against this onslaught of creative inputs, it's not a question of <em>if</em> things will go sideways, but <em>when</em> your proverbial bar bursts into flames, potentially on the most mundane of "customer" requests.</p> <p>The core issue with relying on LLM-based "vibe-testing" is that it's:</p> <ul> <li><strong>Stochastic:</strong> What works once might not work again. You cannot build reliable systems on a foundation of "maybe."</li> <li><strong>Slow &amp; Expensive:</strong> Each "test" involves LLM interactions, racking up latency and API costs. A proper test suite should be efficient.</li> <li><strong>Opaque:</strong> When something breaks, pinpointing the cause—is it your server, the LLM's interpretation, the agent framework, or the prompt?—becomes a frustrating detective game.</li> <li><strong>Superficial:</strong> Natural language interactions rarely achieve the comprehensive coverage needed to find subtle bugs or validate all edge cases.</li> </ul> <p>It's imperative that your server's logic is either impeccably clear or that its error messages are so precise they can effectively guide an LLM back on track. Neither of these is achievable without rigorous, focused testing. While iterating on your instructions to help LLMs "do the right thing" is valuable, robust server-side logic and error handling are non-negotiable.</p> <h3>Testing is Trust (and Good Engineering)</h3> <p>I was incredibly fortunate to start Prefect alongside <a href="https://www.linkedin.com/in/whitecdw/">Chris White</a>, who instilled in me a deep appreciation for the true value of testing. Proper testing serves a deeper purpose than merely affirming your code runs; it's a fundamental practice for <strong>documenting behavior</strong>, <strong>preventing regressions</strong>, and building <strong>deep trust</strong> in your codebase.</p> <p>Chris's philosophy, which we can bring to bear here, emphasizes that:</p> <ul> <li>Unit tests should be <em>atomic</em>, targeting the smallest possible unit of behavior.</li> <li><em>Tests and design go hand-in-hand:</em> if something is hard to test, its design might be flawed. Test-driven development can be particularly effective when defining new user-facing contracts.</li> <li>Tests must <em>clearly document</em> the behavior and expectations that are important to your application. A failing test's <em>title alone</em> should strongly indicate what's broken.</li> <li>Tests should verify <em>expected, assertable behavior</em>, rather than being tightly coupled to specific implementation details. This allows for refactoring with confidence.</li> <li>Critically, tests should <em>not unnecessarily block future paths</em> or refactors. They guard core contracts, not incidental details, fostering an environment built for change.</li> </ul> <p>This philosophy is about creating a safety net that allows for rapid iteration and confident development. When your MCP server is the component bridging the deterministic world of your code with the probabilistic world of LLMs, this trust and safety net become absolutely paramount.</p> <h3>In-Memory Testing with FastMCP</h3> <p>FastMCP 2.0 was designed to make rigorous testing easy, not an afterthought. The key to this is FastMCP's support for <strong>in-memory testing.</strong></p> <p>With FastMCP, you can instantiate a <code>fastmcp.Client</code> and connect it <em>directly</em> to your <code>FastMCP</code> server instance by providing the server as the client's transport target:</p> <pre><code>from fastmcp import FastMCP, Client mcp = FastMCP(name="My MCP Server") @mcp.tool() def add(a: int, b: int) -&gt; int: return a + b test_client = Client(mcp) # Connects the client directly to the server instance </code></pre> <p>This direct, in-memory connection is a game-changer for testing MCP servers because:</p> <ul> <li>💨 <strong>There's no network overhead:</strong> Communication is as fast as a direct Python call.</li> <li>🧘 <strong>No subprocess management is needed:</strong> You don't have to start and stop external server processes for your tests.</li> <li>🎯 <strong>You're testing your actual server logic:</strong> No mocks or simplified protocol implementations are needed; this uses the real STDIO transport internally for maximum fidelity.</li> </ul> <p>Once you have this <code>test_client</code>, you can use its methods to interact with your server just like an LLM, but with the benefit of repeatable determinism and low latency. For example, within an <code>async with test_client:</code> block, you can:</p> <ul> <li>Ping the server: <code>is_alive = await test_client.ping()</code></li> <li>List available tools: <code>tools = await test_client.list_tools()</code></li> <li>Call a specific tool: <code>response = await test_client.call_tool("add", {"a": 1, "b": 2})</code></li> <li>Read a resource: <code>content = await test_client.read_resource("resource://your/data")</code></li> </ul> <p>...and more, including advanced MCP features like logging, progress reporting, and LLM client sampling. Please review FastMCP's <a href="https://gofastmcp.com/clients/client">client docs</a> for more details.</p> <p>This direct, in-memory connection is a game-changer for testing MCP servers because it means your tests are not just validating isolated functions; they're confirming your server's behavior through the actual MCP interaction layer, albeit without network latency.</p> <p>The result? Your tests become:</p> <ul> <li>⚡ <strong>Blazingly Fast:</strong> Run them as part of your normal <code>pytest</code> suite in milliseconds.</li> <li>🧪 <strong>Deterministic:</strong> Get consistent, repeatable results every single time.</li> <li>🎯 <strong>Focused:</strong> Isolate and test your server's tool, resource, and prompt logic precisely.</li> <li>🐍 <strong>Pythonic:</strong> Write your tests using the testing tools and patterns you already know and love.</li> </ul> <p>You'll find yourself writing <em>more</em> tests, not fewer, because testing your MCP functionality becomes as quick and easy as testing any other Python function. Since everything runs in-process, you can use mocks, fixtures, and other familiar testing tools without hesitation.</p> <p>Here's how you can structure your tests using <code>pytest</code>:</p> <pre><code># tests/test_server.py import pytest from fastmcp import FastMCP, Client from mcp.types import TextContent # For type checking results # A reusable fixture for our MCP server @pytest.fixture def mcp_server(): mcp = FastMCP(name="CalculationServer") @mcp.tool() def add(a: int, b: int) -&gt; int: return a + b return mcp # A straightforward test of our tool async def test_add_tool(mcp_server: FastMCP): async with Client(mcp_server) as client: # Client uses the mcp_server instance result = await client.call_tool("add", {"a": 1, "b": 2}) assert isinstance(result[0], TextContent) assert result[0].text == "3" </code></pre> <p>&lt;br/&gt;</p> <p>&lt;Callout color='red'&gt; <strong>Nerd note:</strong> we did not put the client in a fixture, like this:</p> <pre><code># Don't do this! @pytest.fixture async def client(mcp_server: FastMCP): async with Client(mcp_server) as client: yield client </code></pre> <p>That's because <code>pytest</code>'s async fixtures and tests can run in <strong>different event loops</strong>. This can lead to runtime errors related to task cancellation when the <code>Client</code>'s <code>async with</code> block (which manages an <code>anyio</code> task group from the underlying MCP SDK) spans across these different loops. Instantiating the client directly within the test function ensures it operates within the test's event loop. &lt;/Callout&gt;</p> <p>This robust approach allows you to comprehensively test:</p> <ul> <li>Correct tool logic for a wide range of valid inputs (your "lizard" cases!).</li> <li>Graceful error handling for invalid inputs or internal server exceptions.</li> <li>Accurate content delivery for your static resources and dynamic resource templates.</li> <li>Correct rendering of prompts with various parameter combinations.</li> <li>Complex interactions involving the <code>Context</code> object, such as logging, progress reporting, and inter-resource data access.</li> </ul> <p>Instead of merely hoping your LLM client interprets things correctly, you are <em>asserting</em> that your server behaves exactly as designed under a multitude of conditions.</p> <h3>Beyond FastMCP: Testing Any MCP Server</h3> <p>The <code>fastmcp.Client</code> isn't limited to in-memory testing of FastMCP servers you built yourself. It's a versatile tool for interacting with <em>any</em> MCP-compliant server. This means you can write expansive tests for any MCP behavior you want to ensure is reliable and consistent, regardless of the server's implementation.</p> <p>In addition to supplying the client with an explicit transport configuration (like <code>StdioTransport</code> or <code>StreamableHttpTransport</code>), you can often rely on its ability to automatically infer the appropriate transport based on the URL or command string you provide. In the following example, all client objects expose the exact same interface for testing, regardless of how they are instantiated:</p> <pre><code>from fastmcp import Client # A remote server async def test_remote_mcp_server(): async with Client("http://some.api.service/mcp_endpoint") as client: await client.call_tool("some_tool", {"key": "value"}) # A local Node.js server script async def test_local_js_server(): async with Client('path/to/local/server.js') as client: await client.read_resource("resource://path/to/resource") # Two remote servers configured via an MCP config into a FastMCP proxy server async def test_mcp_config_server(): mcp_config = { 'mcpServers': { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "&lt;YOUR_TOKEN&gt;"} }, 'paypal': {'url': 'https://mcp.paypal.com/sse'} } } # The client will infer to create a FastMCPProxy for this config async with Client(mcp_config) as client: await client.call_tool("github_get_user_repos", {"username": "jlowin"}) </code></pre> <p>Your MCP servers form a critical layer in your AI stack. They are the deterministic bedrock upon which the more unpredictable LLM interactions are built. If this foundation is unreliable, your entire AI application becomes fragile.</p> <p>FastMCP's testing capabilities, especially its in-memory testing, are designed to help you build this foundation with confidence and rigor. Stop relying on "looks good to me" vibe-checks through a chat window. Start writing focused, repeatable tests that prove your server does exactly what it's supposed to do.</p> <p>Your AI, your users, and your sanity will thank you for it.</p> <hr /> <p><em>Take control of your MCP server:</em></p> <ul> <li>Star <strong><a href="https://github.com/jlowin/fastmcp">FastMCP on GitHub</a></strong> and explore the <strong><a href="https://gofastmcp.com">docs</a></strong>.</li> <li>Get started: <code>uv pip install fastmcp</code>.</li> </ul> Over the Horizonhttps://jlowin.dev/blog/horizon/https://jlowin.dev/blog/horizon/Automation for the Context EraMon, 01 Dec 2025 00:00:00 GMT<p>Every company will have a context layer.</p> <p>We're building Prefect Horizon to make that possible.</p> <p>The context layer is where your AI agents interface with your business. It's where teams expose their proprietary data, tools, and workflows to autonomous systems. MCP is the technology that makes this possible, and we spent the last year making FastMCP the standard framework for working with it.</p> <p>We've watched FastMCP grow to more than a million downloads a day. We've watched users build tens of thousands of servers on FastMCP Cloud. And we've watched what happens when MCP scales in an organization. Every company we talk to hits the same walls: "Where do I host these servers? How do I know what's been deployed? Who's allowed to access what?"</p> <p>We built Horizon to solve this problem. Its core is Prefect's enterprise MCP gateway: deployment, registries across internal and external servers, and governance down to the tool level. It will be widely available early next year.</p> <p>On that foundation, we're building the innovations that will power your context layer. Remix any combination of servers into curated endpoints for each use case. Build playbooks that progressively disclose tools as agents navigate complex workflows. Give business users an agentic interface to your company without ever knowing what "MCP" is.</p> <p>We've seen what's coming.</p> <p>See you over the Horizon.</p> Most MCP Usage is Invisiblehttps://jlowin.dev/blog/internal-mcp/https://jlowin.dev/blog/internal-mcp/What you don't see is what you getTue, 02 Dec 2025 00:00:00 GMT<p>Most MCP usage is invisible.</p> <p>Many of us (myself included!) expected MCP to take off as something like an "App Store" for agent-facing business logic. Companies publish servers, customers use them, ecosystems form, ???, profit.</p> <p>That may still happen. But it's not what's happening now.</p> <p>Today, there's a small number of widely-used public servers - GitHub, Linear, maybe a handful others - and then a very long tail of servers with one or zero users. If you're casually observing MCP usage, you'd be forgiven for thinking adoption is quite limited.</p> <p>But inside modern organizations, it's a different story. The use of MCP to serve internal data and workflows has exploded, with first-party servers solving proprietary problems. The protocol has become the standard for internal connectivity, not external distribution, and it isn't visible on public registries because it isn't meant for the public.</p> <p>This is where the real activity is, and it's far beyond what most people realize.</p> The Inverted Agenthttps://jlowin.dev/blog/the-inverted-agent/https://jlowin.dev/blog/the-inverted-agent/How a boring MCP spec update flips the AI stack upside downFri, 05 Dec 2025 00:00:00 GMT<p>I think <strong>SEP-1577</strong> is the sleeper hit of the new Model Context Protocol (MCP) specification.</p> <p>Hidden behind a dry title ("Sampling with Tools") is a feature that enables a complete architectural inversion of how we build and deploy AI agents.</p> <p>"Sampling" is the mechanism by which an MCP server asks the client's LLM to generate text (e.g., "Hey Claude, summarize this data"). When I started building FastMCP 2.0 back in April 2025, this was the feature that excited me the most -- and <a href="https://jlowin.dev/blog/fastmcp-2#-client-infrastructure--llm-sampling">here's proof</a>!</p> <p>But as far as I can tell, it has a grand total of approximately one power user: FastMCP maintainer <strong>Bill Easton</strong>.</p> <p><em>(Edit: an hour after posting this, I found the other power user. Unsurprisingly, it's Angie Jones, who just shared a <a href="https://block.github.io/goose/blog/2025/12/04/mcp-sampling/">fantastic blog post</a> on MCP sampling.)</em></p> <p>While the rest of us were building standard tools, Bill was pushing sampling as far as it could go. He hacked together tool calling, structured results, and agentic loops on top of the previous, very limited version of the protocol. He saw the potential before the spec even supported it.</p> <p>Now, with <a href="https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577">SEP-1577</a>, the official spec has caught up to Bill's vision. And the more time I spend with it, the more mind-bending I find it. It looks and feels exactly like every agent framework I've ever used, but the deployment model is completely backwards.</p> <h3>How We Build Agents Today</h3> <p>To understand the shift, look at how we build agents today.</p> <p>In frameworks like Pydantic AI, LangChain, or Prefect's very own <a href="https://github.com/PrefectHQ/marvin">Marvin</a>, the "Agent" is a capital-C-<strong>Client</strong>. It is a Python script running on your machine. It holds the state, the system prompt, and the loop that decides which steps to take next.</p> <p>In this model, the <strong>Server</strong> provides remote LLM completion functionality. It does not run custom code, dictate logic, or even hold state. The Client orchestrates all activity.</p> <p>This works, but it has a massive <strong>distribution problem</strong>. If I want to share my "Code Janitor Agent" with you, I have to send you a repository. You have to install Python, manage dependencies, set up environment variables, and run the script. The "Agency" is locked inside my local environment.</p> <h3>Flip It</h3> <p>SEP-1577 flips this stack upside down.</p> <p>It allows an MCP Server to define a sampling request that <em>includes tools</em>. The Server can now say to the Client:</p> <blockquote> <p>"Here's a goal, and here are the tools you need to achieve it. You provide the raw intelligence, but I'll control the flow."</p> </blockquote> <p>The Server holds the prompt. The Server holds the workflow logic. The Server holds the tools. It can change them at any time.</p> <p>When you connect a generic client—like Claude Desktop, Cursor, or a simple IDE plugin—to this server, the client doesn't need to know <em>anything</em> about the agent's logic. It just acts as the compute engine. The Server effectively <strong>"borrows" the Client's LLM</strong> to drive its own internal agent.</p> <h3>But Text Is Useless</h3> <p>There's a problem with raw sampling: it returns natural language text.</p> <p>MCP servers are programmatic. They need to parse, validate, and act on data. Getting back "The temperature is about 72 degrees and it's partly cloudy" is almost useless as a building block—you'd have to parse the text all over again just to extract the values.</p> <p>FastMCP solves this by layering structured output on top of SEP-1577's sampling primitives:</p> <pre><code>from pydantic import BaseModel class Weather(BaseModel): temperature: float conditions: str @mcp.tool async def get_weather(city: str, ctx: Context) -&gt; Weather: result = await ctx.sample( f"What is the current weather in {city}?", result_type=Weather, ) return result.result </code></pre> <p>The server borrows the client's LLM, but gets back typed, validated data it can actually use. No parsing. No hoping the format is right. Just a <code>Weather</code> object.</p> <p>This alone makes sampling practical. But the real power comes when you add tools.</p> <h3>Now Add Tools</h3> <p>Layer in tools, and things get interesting. We are adding first-class support for this in FastMCP, and what's wild is how familiar the code looks. You write what looks like a standard client-side agent loop, but you deploy it as a server-side tool.</p> <p>Here is what it looks like to build a research agent that uses structured output and tools, running entirely on the server:</p> <pre><code>from fastmcp import FastMCP, Context from fastmcp.server.sampling import sampling_tool from pydantic import BaseModel mcp = FastMCP("Research Agent") # Define the output schema class ResearchReport(BaseModel): summary: str sources: list[str] # Define a helper tool for the agent @sampling_tool def search_web(query: str) -&gt; str: """Search the web for information.""" return f"Results for: {query}" @mcp.tool async def generate_report(topic: str, ctx: Context) -&gt; ResearchReport: """A tool that acts as an autonomous research agent.""" # The server orchestrates the loop! result = await ctx.sample( messages=[f"Research {topic} and summarize."], tools=[search_web], result_type=ResearchReport, max_iterations=5, ) return result.result </code></pre> <p>If you've used Pydantic AI or Marvin, this pattern—passing tools and a result type to an LLM—is second nature.</p> <p>The difference is that <strong>this isn't a script.</strong> It's a tool on an MCP server.</p> <p>Because of this, I don't need to ship you a Python environment to run this agent. I just give you the server connection. You connect Claude Desktop to it, ask "Generate a report on FastMCP," and <em>your</em> Claude instance instantly knows how to perform the research, call the web search tool, loop 5 times, and return the structured report.</p> <h3>Universal Clients</h3> <p>We are moving from a world of <strong>"Thick Clients"</strong> to <strong>"Universal Clients."</strong></p> <p>This solves the distribution problem for complex agentic workflows. You can wrap sophisticated logic—loops, chains of thought, structured validation—inside a standard MCP server. Any client that connects instantly "becomes" that agent.</p> <p>It is effectively "Write Once, Run Anywhere" for AI agents.</p> <p>We are shipping support for this in <a href="https://github.com/jlowin/fastmcp/pull/2551">FastMCP</a> as soon as the upstream SDK creates a stable foundation for it. Until then, keep an eye on the repo... and maybe send Bill a thank you note.</p> Centuries of Pain and Sorrowhttps://jlowin.dev/blog/memes/centuries-of-pain-and-sorrow/https://jlowin.dev/blog/memes/centuries-of-pain-and-sorrow/Fri, 04 Oct 2024 00:00:00 GMT<p><img src="./centuries-of-pain-and-sorrow.jpg" alt="" /></p> Does o1 Mean Agents Are Dead?https://jlowin.dev/blog/does-o1-mean-agents-are-dead/https://jlowin.dev/blog/does-o1-mean-agents-are-dead/Reasoning is an iterative behavior, but it's not agentic.Fri, 13 Sep 2024 09:00:00 GMT<p>OpenAI's o1's reasoning ability is essentially chain-of-thought behind the API, which makes it a much more powerful model for handling nuanced problems. Chain-of-thought invites the LLM to iteratively "think out loud" by revisiting its internal monologue until it has produced a satisfactory answer. We know that this produces superior results to one-shot responses, and here it's been productized.</p> <p>Agents are also characterized by iterative behavior. But there's a key difference: while models like o1 iterate internally to refine their reasoning, agents engage in iterative interactions with the external world. They perceive the environment, take actions, observe the outcomes (or side effects) and adjust accordingly. This recursive process enables agents to handle tasks that require adaptability and responsiveness to real-world changes.</p> <p>So o1 implements a behavior that formerly we would have used a simple agentic workflow to mimic, at greater expense and latency. This is good! But internal reasoning does not replace the <em>outcome-driven behaviors</em> that characterize the promise of AI agents.</p> <p>In the limit, if o1 was itself an "agent" by any definition, capable of acting on its own, we would still want to formalize methods of deploying it against a specific objective in a repeatable, observable manner.</p> <p>&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Does OpenAI's o1 mean agents are dead?&lt;br/&gt;&lt;br/&gt;o1's reasoning ability is essentially chain-of-thought behind the API, which makes it a much more powerful model for handling nuanced problems. Chain-of-thought invites the LLM to iteratively "think out loud" by revisiting its internal…&lt;/p&gt;— Jeremiah Lowin (@jlowin) &lt;a href="https://twitter.com/jlowin/status/1834722014839418962?ref_src=twsrc%5Etfw"&gt;September 13, 2024&lt;/a&gt;&lt;/blockquote&gt;</p> An Open-Source Maintainer's Guide to Saying Nohttps://jlowin.dev/blog/oss-maintainers-guide-to-saying-no/https://jlowin.dev/blog/oss-maintainers-guide-to-saying-no/Stewardship in the age of cheap codeSat, 13 Sep 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>One of the hardest parts of maintaining an open-source project is saying <strong>"no"</strong> to a good idea. A user proposes a new feature. It’s well-designed, useful, and has no obvious technical flaws. And yet, the answer is <strong>"no."</strong> To the user, this can be baffling. To the maintainer, it’s a necessary act of stewardship.</p> <p>Having created and maintained two highly successful open-source projects, <a href="https://github.com/PrefectHQ/prefect">Prefect</a> and <a href="https://github.com/jlowin/fastmcp">FastMCP</a>, helped establish a third in Apache Airflow, and cut my OSS teeth contributing to Theano, I’ve learned that this stewardship is the real work. The ultimate success of a project isn't measured by the number of features it has, but by the coherence of its vision and whether it finds resonance with its users. As Prefect's CTO <a href="https://www.linkedin.com/in/whitecdw/">Chris White</a> likes to point out:</p> <blockquote> <p>"People choose software when its abstractions agree with their mental model."</p> </blockquote> <p>Your job as an open-source maintainer is to first establish that mental model, then relentlessly build software that reflects it. A feature that is nominally useful but not spiritually aligned can be a threat just as much as an enhancement.</p> <p>This threat can take many forms. The most obvious is a feature that's wildly out of scope, like a request to add a GUI to a CLI tool -- a valid idea that likely belongs in a separate project. More delicate is the feature that brilliantly solves one user's niche problem but adds complexity and maintenance burden for everyone else. The most subtle, and perhaps most corrosive, is the API that's simply "spelled" wrong for the project: the one that breaks established patterns and creates cognitive dissonance for future users. In many of the projects I've been fortunate to work on, both open- and closed-source, we obsess over this because a consistent developer experience is the foundation of a framework that feels intuitive and trustworthy.</p> <p>So how does a maintainer defend this soul, especially as a project scales? It starts with documenting not just how the project works, but why. Clear developer guides and statements of purpose are your first line of defense. They articulate the project's philosophy, setting expectations before a single line of code is written. This creates a powerful flywheel: the clearer a project is about why it exists, the more it attracts contributors who share that vision. Their contributions reinforce and refine that vision, which in turn justifies the project’s worldview. Process then becomes a tool for alignment, not bureaucracy. As a maintainer, you can play defense on the repo, confident that the burden of proof is on the pull request to demonstrate not just its own value, but its alignment with a well-understood philosophy.</p> <p>This work has gotten exponentially harder in the age of LLMs. Historically, we could assume that since writing code is an expensive, high-effort activity, contributors would engage in discussion before doing the work, or at least seek some sign that time would not be wasted. Today, LLMs have inverted this. Code is now cheap, and we see it offered in lieu of discourse. A user shows up with a fully formed PR for a feature we've never discussed. It's well-written, it "works," but it was generated without any context for the framework's philosophy. Its objective function was to satisfy a user's request, not to uphold the project's vision.</p> <p>This isn't to say all unsolicited contributions are unwelcome. There is nothing more delightful than the drive-by PR that lands, fully formed and perfectly aligned, fixing a bug or adding a small, thoughtful feature. We can't discourage these contributors. But in the last year, the balance of presumption has shifted. The signal-to-noise ratio has degraded, and the unsolicited PR is now more likely to be a high-effort review of a low-effort contribution.</p> <p>So what's the playbook? In FastMCP, we recently tried to nudge this behavior by requiring an issue for every PR. In a perfect example of <a href="https://en.wikipedia.org/wiki/Unintended_consequences">unintended consequences</a>, we now get single-sentence issues opened a second before the PR... which is actually worse. More powerful than this procedural requirement is sharing a simple sentence that we are unconvinced that the framework should take on certain responsibilities for users. If a contributor wants to convince us, we all only benefit from that effort! But as I wrote earlier, the burden of proof is on the contributor, never the repo.</p> <p>A more nuanced pushback against viable code is that as a maintainer, you may be uncomfortable or unwilling to maintain it indefinitely. I think this is often forgotten in fast-moving open-source projects: there is a significant transfer of responsibility when a PR is merged. If it introduces bugs, confusion, inconsistencies, or even invites further enhancements, it is usually the maintainer who is suddenly on the hook for it. In FastMCP, we've introduced and documented the <code>contrib</code> module as one solution to this problem. This module contains useful functionality that may nonetheless not be appropriate for the core project, and is maintained exclusively by its author. No guarantee is made that it works with future versions of the project. In practice, many contrib modules might have better lives as standalone projects, but it's a way to get the ball rolling in a more communal fashion.</p> <p>One regret I have is that I observe a shift in my own behavior. In the early days of Prefect, we did our best to maintain a 15-minute SLA on our responses. Seven years ago, a user question reflected an amazing degree of engagement, and we wanted to respond in kind. Today, if I don't see a basic attempt to engage, I find myself mirroring that low-effort behavior. Frankly, if I'm faced with a choice between a wall of LLM-generated text or a clear, direct question with an MRE, I'll take the latter every time.</p> <p>I know this describes a fundamentally artisanal, hand-made approach to open source that may seem strange in an age of vibe coding and YOLO commits. I'm no stranger to LLMs. Quite the opposite. I use them constantly in my own work and we even have an AI agent (hi Marvin!) that helps triage the FastMCP repo. But in my career, this thoughtful, deliberate stewardship has been the difference between utility projects and great ones. We used to call it "community" and I'd like to ensure it doesn't disappear.</p> <p>I think I need to be clear that <em>nothing in this post should be construed as an invitation to be rude or to stonewall users</em>. As an open-source maintainer, you should be ecstatic every time someone engages with your project. After all, if you didn't want those interactions, you could have kept your code to yourself! The goal in scalable open-source must always be to create a positive, compounding community, subject to whatever invitation you choose to extend to your users. Your responsibility is to ensure that today's <strong>"no"</strong> helps guide a contributor toward tomorrow's enthusiastic <strong>"yes!"</strong></p> <p>When this degree of thoughtfulness is well applied, it translates into a better experience for all users—into software whose abstractions comply with a universal mental model. It's a reminder that this kind of stewardship is worth fighting for.</p> <p>Two weeks ago, I was in a room that reminded me this fight is being won at the highest level. I had the opportunity to join the MCP Committee for meetings in New York and saw a group skillfully navigating a version of this very problem. MCP is a young protocol, and its place in the AI stack has been accelerated more by excitement than its own maturity. As a result, it is under constant assault that it should simultaneously do more, do less, and everything in between.</p> <p>A weak or rubber-stamp committee would be absolutely overwhelmed by this pressure, green-lighting any plausible feature to appease the loudest voices in this most-hyped corner of tech. And yet, over a couple of days, what I witnessed was the opposite. The most important thing I saw was a willingness to debate, and to hold every proposal up to a (usually) shared opinion of what the protocol is supposed to be. There was an overriding reverence for MCP's teleological purpose: what it should do and, more critically, what it should <em>not</em> do. I especially admired <a href="https://x.com/dsp_">David's</a> consistent drumbeat as he led the committee: "That's a good idea. But is it part of the protocol's responsibilities?"</p> <p>Sticking to your guns like that is the hard, necessary work of maturing a technology with philosophical rigor. I left New York more confident than ever in the team and MCP itself, precisely because of how everyone worked not only to build the protocol, but to act as its thoughtful custodians. It was wonderful to see that stewardship up close, and I look forward to seeing it continue in open-source more broadly.</p> Don't Call It an Officehttps://jlowin.dev/blog/dont-call-it-an-office/https://jlowin.dev/blog/dont-call-it-an-office/Rethinking collaboration in a remote-first worldThu, 05 Dec 2024 00:00:00 GMT<p>import { Image } from "astro:assets"; import bridge from "./bridge.png"; import chicago from "./chicago.png"; import embassy from "./embassy.png"; import lab from "./lab.png";</p> <p>Prefect has been a remote company since <strong>day 15</strong>.</p> <p>For the first 14 days, our small team worked side by side in DC, building what I envisioned as the city's next great tech company. Then, when our CTO Chris moved to San Francisco, we established what would become one of our core principles: <em>a company is remote if it has one remote employee.</em></p> <p>This wasn't merely a semantic distinction. The principle stemmed from our conviction that no team member should face disadvantages based on their location. Even as we built a predominantly DC-based team in the following years, we maintained our identity as a remote-first company. In the pre-COVID era, this stance raised eyebrows – the idea that having just one remote employee would define our entire operating model.</p> <p>What we discovered, and what many companies would later learn during the pandemic, was that enabling remote work wasn't the real challenge. The challenge was creating a culture compelling enough that people would choose to come together even when they didn't have to. Being remote-first doesn't mean rejecting physical spaces entirely – it means ensuring that any investment in physical space actively supports our mission without creating advantages or disadvantages based on location. This insight would eventually lead us to fundamentally reimagine how companies should think about physical space – not as infrastructure to be managed, but as strategic assets that must earn their keep while preserving the equality of our distributed culture.</p> <h2>Beyond Hybrid</h2> <p>Many companies now attempt to balance remote and in-person work through hybrid models. While well-intentioned, these approaches often highlight the challenge of bridging two distinct workplace cultures, and the result is the "worst of both worlds", epitomized by employees who commute to an office only to join the same video call as everyone else. Furthermore, not only do colocated team members enjoy a significant reduction in friction for small requests, but their remote colleagues frequently pay a "small-talk tax" in the form of socially-enforced preambles to every conversation.</p> <p>When COVID forced companies to go remote, we had an advantage – we'd already been thinking deliberately about how to balance a remote workforce with a strong in-person contingent. We focused intently on <a href="https://www.prefect.io/blog/dont-panic-the-prefect-guide-to-building-a-high-performance-team">building a remote culture</a> that even our DC-based team, who explicitly preferred being in-person, would want to actively participate in. This approach was so successful that we opted to step through a "one-way door" and begin hiring primarily outside of DC. We knew that once we made that decision, we could never go back to being predominantly office-based. But being remote-first didn't mean abandoning physical spaces entirely – it meant being more thoughtful about how and why we used them.</p> <h2>The Chicago Experiment</h2> <p>Perhaps surprisingly, that philosophy led us to establish an office in Chicago's River North neighborhood. Unlike traditional real estate expansions driven by headcount projections or market presence requirements, this was a deliberate experiment in creating purpose-driven space. Something unexpected happened: "The 505" emerged as more than just another workplace – it became our blueprint for focused collaboration. Its location – no more than three and a half hours from any Prefect team member – proved to be an unexpected advantage, eliminating the logistical complexity and expense of traditional offsites. Teams of any size could drop in for a few days of productive work, following a proven playbook that balanced focused collaboration with team bonding. The space's natural gravity even attracted some of our younger team members to relocate there, creating an organic hub that enhanced rather than replaced our remote culture.</p> <p>&lt;figure class="flex flex-col items-center"&gt; &lt;Image src={chicago} alt="The 505 in Chicago" width={500} /&gt; &lt;figcaption class="text-center"&gt;<strong>The 505</strong> in Chicago&lt;/figcaption&gt; &lt;/figure&gt;</p> <p>What made the Chicago space different wasn't just its location – it was that it emerged with an organic purpose. Unlike traditional corporate real estate that companies acquire simply to have a presence, The 505 had proven its value through measurable impact on our team dynamics and culture. It became our first case study in what happens when you let space earn its role rather than prescribing it.</p> <p>After our most recent all-hands gathering in Chicago, which was an extraordinary success by any measure, Brad, our SVP of People, pulled me aside. <em>"I think it's time for us to figure out how to capture this feeling more frequently,"</em> he said.</p> <p>That conversation sparked a fundamental shift in how we think about physical space. Brad's insight was profound: what if we treated physical spaces with the same strategic rigor as executive hires? Just as each senior leader joins with clear objectives and accountability for outcomes, each space could have specific KPIs and a falsifiable hypothesis about its value. This wasn't just about real estate – it was about creating strategic assets that would either prove their worth or be retired, just like any other investment. Moreover, it required us to approach each new space with the same level of intentionality as recruiting a new executive.</p> <p>Based on the lessons from Chicago, we developed a framework that would transform our approach to evaluating spaces. We began looking beyond traditional metrics like square footage and cost per desk, focusing instead on each location's potential to catalyze specific types of collaboration and community. Most importantly, each space would need an ambassador – a leader responsible for delivering on its mission and accountable for its success metrics. Just as we wouldn't hire a senior leader without a clear mandate and performance expectations, we wouldn't lease space without a defined mission and success criteria.</p> <h2>Three New Spaces</h2> <p>Today we're putting this framework into action. After months of careful evaluation, we're launching three new spaces, each "hired" with a distinct strategic purpose and clear success metrics. We deliberately avoid calling them "offices" – that term suggests nothing more than "a place to do work," which misses the point entirely. If there's anything a remote company doesn't lack, it's places to work! Instead, these spaces are strategic assets with specific missions, more akin to specialized facilities than traditional offices. Each one has been carefully designed to catalyze particular types of collaboration and drive specific outcomes that are impossible to achieve remotely.</p> <h3>The Embassy (Washington, DC)</h3> <p>The Embassy's mission is to strengthen Prefect's roots in the DC tech ecosystem. Located in one of the city's newest and most eco-friendly buildings, it overlooks Pennsylvania Avenue just blocks from the White House. The Embassy features multiple private offices for focused or collaborative work, as well as indoor and outdoor space to host over 100 people for community events and substantive discussions. As its ambassador, I'll use this as my permanent base to engage with customers, government representatives, investors, and the broader tech community — not to mention Prefect's own team! Success will be measured not just in utilization metrics, but in concrete outcomes: new partnerships formed, policy initiatives advanced, and meaningful connections established that demonstrably advance Prefect's presence in the national tech conversation.</p> <p>&lt;figure class="flex flex-col items-center"&gt; &lt;Image src={embassy} alt="The Embassy in DC" width={500} /&gt; &lt;figcaption class="text-center"&gt;<strong>The Embassy</strong> in DC&lt;/figcaption&gt; &lt;/figure&gt;</p> <h3>The Bridge (New York City)</h3> <p>Led by our VP of Product Adam Azzam, the Bridge establishes our presence in the center of one of the world's most dynamic centers of data innovation. Its mission goes beyond traditional product development – it's about creating a living laboratory for product evolution. When practitioners from healthcare, finance, gaming, and AI gather to discuss product direction, the cross-pollination of ideas and immediate feedback loops are invaluable. Success here will be measured in the speed and quality of product iterations, the depth of customer insights gathered, and the tangible impact on our product roadmap.</p> <p>&lt;figure class="flex flex-col items-center"&gt; &lt;Image src={bridge} alt="The Bridge in NYC" width={500} /&gt; &lt;figcaption class="text-center"&gt;<strong>The Bridge</strong> in NYC&lt;/figcaption&gt; &lt;/figure&gt;</p> <h3>The Lab (Half Moon Bay)</h3> <p>Our most unconventional experiment, the Lab, isn't a traditional workplace at all. Led by Chris (bringing our story full circle to California), it's a self-sustaining farm turned high-tech workspace on the coast. The setting combines modern collaborative facilities with farm-to-table dining, walking trails, and a beautiful setting to create an environment that encourages both focused technical work and creative thinking. Engineering teams will use this space for intensive development sessions, with success measured in breakthrough features developed, architectural decisions made, and the acceleration of our technical roadmap. Like any experimental initiative, its continued existence will depend on its ability to demonstrate concrete value to our engineering velocity and innovation.</p> <p>&lt;figure class="flex flex-col items-center"&gt; &lt;Image src={lab} alt="The Lab in Half Moon Bay" width={500} /&gt; &lt;figcaption class="text-center"&gt;<strong>The Lab</strong> in Half Moon Bay&lt;/figcaption&gt; &lt;/figure&gt;</p> <h2>Measuring Success</h2> <p>These spaces are carefully designed experiments with clear hypotheses and measurable outcomes. Each has been conceived with the same rigor we'd apply to any strategic hire or major initiative. Just as we regularly evaluate the performance and impact of our senior leaders, we'll continuously assess these spaces against their defined objectives. This isn't about creating permanent monuments to our company; it's about launching strategic initiatives that must continuously prove their value.</p> <p>Crucially, none of these spaces create a "second-class" remote workforce. We're not hybrid, with some employees tethered to desks while others work from home. We're not remote-only, rejecting the value of physical collaboration entirely. We're remote-first: our company functions completely asynchronously, but we strategically invest in spaces that catalyze specific kinds of valuable in-person interactions – as long as participation in those interactions remains entirely optional and never becomes a prerequisite for success at Prefect.</p> <p>This approach represents a fundamental shift in how companies think about physical space in a remote-first world. Rather than treating real estate as necessary infrastructure or compromising with half-measures like hot-desking, we're approaching physical spaces as strategic investments with the same scrutiny, expectations, and accountability we apply to our most senior hires. Each space must continuously prove its value proposition while preserving our distributed culture – a standard that few traditional workplaces could meet.</p> <p>The principle that guided us on day 15 still holds: <em>when a company has one remote employee, the entire company is remote</em>. But that doesn't mean we can't be intentional about creating physical spaces that enhance our ability to collaborate, innovate, and build community. The key is ensuring these spaces serve a purpose beyond just existing – each needs an ambassador and a mission, transforming them from simple real estate into strategic assets that help us achieve specific goals.</p> <p>As we launch these experiments, we're excited to learn what works and what doesn't. Some spaces may exceed our expectations; others may need to be reimagined or retired – just like any other strategic investment. What we know for certain is that we're not going back to traditional models. Instead, we're moving forward with a new framework for physical space – one that prioritizes purpose over presence, mission over location, and pull over push.</p> <p>Just don't call them offices.</p> The Qualified Selfhttps://jlowin.dev/blog/the-qualified-self/https://jlowin.dev/blog/the-qualified-self/AI-generated content is everywhere... so I'm starting a blog.Mon, 16 Sep 2024 00:00:00 GMT<p>We're drowning in AI-generated words, a trend that will only accelerate as the technology improves and becomes more accessible. But it's undeniable that LLMs have made it dramatically easier to express, explore, and iterate on ideas. The paradox of modern AI is that while content production has been commoditized <em>on average</em>, the act of creating content has never been more valuable <em>for individuals</em>.</p> <p>This shift in the value of individual content creation mirrors a broader transformation in how we need to start thinking about personal data. For years, many have pursued <strong>"the quantified self"</strong> by collecting structured data about their lives, focusing on metrics like fitness, wealth, and other easily quantifiable measures. But the rise of modern AI has fundamentally changed the nature of valuable data. Today, <strong>words are data</strong> in a way that was never true before, at least not without a significant R&amp;D budget. Words carry meaning and semantics beyond the bytes that represent each character, a property that's always been true for humans and is finally available for machines.[^1]</p> <blockquote> <p>In the new data economy, words are the most valuable currency.</p> </blockquote> <p>The production of this rich, qualitative data—our thoughts, ideas, and insights—results in <strong>the qualified self</strong>. Instead of merely tracking structured data, we're pouring out our experiences in a form that LLMs can analyze and enhance, expanding our understanding of ourselves and our world. But this is about more than analysis. When we work with an LLM, we're not just exchanging information; we're engaging in a novel form of programming. I find it most helpful to think of LLMs as probability distributions, where every interaction conditions that distribution to produce more useful outputs.</p> <p>With this framing, becoming "better" at working with LLMs is about accelerating their probability engines into useful regimes as quickly as possible.</p> <p>This realization has driven me to collect as much of my personal "data" (in this new, qualitative sense) as possible. In the last year, my workflows have evolved to accommodate this new paradigm. I've started transcribing every meeting and, for the first time, keeping journals.[^2] These aren't just records; they're fuel for collaboration with AI assistants. Today's LLMs have very short-term memories. The more relevant "data" I can feed them—my thoughts, decisions, and the context behind them—the more valuable our interactions become. I've found that my daily product journal for <a href="https://controlflow.ai/">ControlFlow</a> is as valuable for accelerating an LLM to understand the present state of the library as it is for letting it know all the approaches we attempted that <em>didn't</em> work out.</p> <p>In a funny way, the qualified self may be the ultimate victory for the schema-on-read crowd. I'm not just hoarding information; I'm building the richest unstructured dataset I can. I believe that the most impactful innovations in the LLM space will come not from more powerful models, but from more capable context management. I'm preparing a dynamic, AI-ready knowledge base of... me.</p> <p>And so, embracing this new paradigm, I've decided to start blogging for the first time in over a decade. I've always believed in <a href="https://joincolossus.com/episode/puttagunta-open-source-crash-course/">open source</a> and <a href="https://twitter.com/patrick_oshag/status/971100425498841089">learning in public</a>, and this blog represents a new commitment to doing so. This writing is as much for my future AI collaborators as it is for any readers that may stumble upon it.</p> <p>This is my qualified self.[^3]</p> <p>[^1]: Lately I've been thinking a lot about Neal Stephenson's <em>Snow Crash</em>. The book is often credited with anticipating the metaverse, but it was equally prescient about the power of language in the digital age. It explores a world where language is more than just communication—it's a fundamental tool of digital control and influence. This concept resonates strongly in our current reality, where the words we use to interact with AI systems can shape their outputs and, by extension, our environment.</p> <p>[^2]: I've started with keeping product journals: what I worked on, what I learned, what didn't work, and what I want to try next. [^3]: ...at least, this is a curated version of my qualified self. My drafts overfloweth; fortunately, my LLMs don't seem to mind.</p> Stop Converting Your REST APIs to MCPhttps://jlowin.dev/blog/stop-converting-rest-apis-to-mcp/https://jlowin.dev/blog/stop-converting-rest-apis-to-mcp/Your auto-generated MCP is bad, and you should feel bad.Thu, 10 Jul 2025 00:00:00 GMT<p>import { Image } from 'astro:assets'; import badFeeling from './bad-feeling.webp';</p> <p>FastMCP's <a href="https://gofastmcp.com/integrations/openapi">OpenAPI converter</a> has become the most popular tool for auto-generating MCP servers from REST APIs. I'm excited about that, because I built the feature to feel like magic: a single line of code to expose your entire REST API to an LLM. It’s the ultimate shortcut, and for quick prototypes, it's amazing.</p> <p>But now I need you to stop using it so much.</p> <p>I've come to realize that it can paper over a fundamental problem: <em>an API built for a human will poison your AI agent.</em> In practice, LLMs achieve significantly better performance with well-designed, tailored MCP servers than with auto-converted ones. The reason goes right to the core of how agents and humans interact with software, and how we design technical products for each consumer.</p> <p>A good REST API is generous. It is a model of discoverability and atomicity. It offers hundreds of single-serving endpoints, flexible parameters, and endless options because programmatic iteration is cheap. Human developers are brilliant at doing discovery once and subseqeuently ignoring what’s irrelevant, and their code can chain together atomic calls -- <code>get_user()</code>, then <code>get_orders(user_id)</code>, then <code>get_order_details(order_id)</code> -- with quick network hops to achieve complex outcomes. For them, more choice is good. We use properties like idempotency, pagination, and caching to make our APIs more efficient in the face of relatively deterministic access patterns.</p> <p>But when you hand this interface to an agent, you're not empowering it; you're drowning it.</p> <p>Agentic iteration is brutally expensive. First, there’s the <strong>literal cost of context</strong>. An LLM must process the name, description, and parameters of every single tool you provide, every single time it reasons. For an agent, many programmatic choices imply a bloated context, and every extra endpoint is a tax paid in tokens and latency on every interaction. Second, <strong>atomicity is an agent anti-pattern</strong>. Each tool call an LLM makes is an expensive round trip involving a full reasoning cycle. Forcing an agent to chain multiple atomic calls is slow, error-prone, and burns through tokens.</p> <p>Moreover, <strong>context pollution is the silent killer of contemporary agentic workflows.</strong> Many users do not realize that their toolkits -- including MCP servers -- may inject thousands more tokens than even their custom system prompts. Your agent stops being a helpful assistant and becomes an obsessive API librarian, endlessly debating the nuances of your endpoints instead of achieving its actual behavioral goal. More tool calls reinforce that behavior, and your agent gets slower, dumber, and more expensive with every interaction.</p> <p>So when I see the community's enthusiasm for auto-generating MCP servers from massive OpenAPI specs, it makes me think:</p> <p>&lt;Image src={badFeeling} alt="I've got a bad feeling about this" class="shadow-lg mx-auto"/&gt;</p> <p>An API that is "sophisticated" for a human is one with rich, composable, atomic parts. An API that is "sophisticated" for an agent is one that is ruthlessly curated and minimalist.</p> <p>We see the practical consequences of this mismatch in FastMCP's GitHub repo almost daily. "The LLM timed out trying to decide between create_invoice and generate_invoice." "My agent hallucinated a get_all_users_with_blue_eyes endpoint because the 50 other user-related tools made it seem plausible." "How do I prevent the LLM from trying to call the DELETE /everything endpoint I forgot was in my spec?" These are the predictable results of a flawed premise and the belief that context should be stuffed, not pruned.</p> <p>The truth is, it's far easier to build a clean, curated MCP server than it is to debug an LLM that's lost in the labyrinth of an auto-generated REST API. As <a href="https://www.linkedin.com/feed/update/urn:li:activity:7343322701446397953/?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7343322701446397953%2C7343333026455539713%29">Maxime Beauchemin recently put it</a>:</p> <blockquote> <p>[We must] not only enumerate but also qualify each service, as it's trivial for anyone to write a quick REST-API wrapper and call it done, where in reality we're discovering that considerations around API design for LLMs are significantly different from the ones we've been using for REST forever.</p> </blockquote> <p>FastMCP's OpenAPI converter <strong>is</strong> a valuable tool for bootstrapping. But we have to be disciplined. We can’t let this convenient shortcut ultimately create more problems than it solves.</p> <p>So, what's the right way forward? The goal isn't to abandon our existing APIs, but to treat them as a source of truth to be carefully translated, not a finished product to be carelessly wrapped.</p> <ol> <li> <p><strong>Bootstrap, Don't Deploy.</strong> Use the <code>FastMCP.from_openapi()</code> feature for what it's truly good for: bootstrapping. Use it to quickly explore what's possible, to see your tools through an agent's eyes, or to run a quick internal demo. But do not ship it to production.</p> </li> <li> <p><strong>Curate Aggressively.</strong> The act of curation is now a core part of building for agents. Instead of exposing the raw tool, use a transformation to craft a new, LLM-friendly version. FastMCP's <code>Tool.from_tool()</code> was built for exactly this. Take that messy <code>generic_search(q, lim, fq, …)</code> tool and transform it into a clean <code>find_products(keyword: str)</code>. Rename cryptic arguments. Hide irrelevant parameters with default values. This is where the real work lies.</p> </li> <li> <p><strong>Start with the Agent Story.</strong> For your most critical workflows, build a new, minimal MCP server from scratch. Don't start with your API spec. Start with the <a href="/blog/as-an-agent-the-new-user-story">agent story</a>: <em>"As an agent, given <code>{context}</code>, I use <code>{tools}</code> to achieve <code>{outcome}</code>."</em> Then, build <em>only</em> the tools required to fulfill that story.</p> </li> </ol> <p>The promise of AI agents isn't just to make our existing software "chatty." It's an opportunity to design cleaner, more intentional, machine-first interfaces. <strong>Stop converting your REST APIs. Start curating them.</strong></p> The Sustainable Startuphttps://jlowin.dev/blog/the-sustainable-startup/https://jlowin.dev/blog/the-sustainable-startup/Transitioning Prefect to a customer-funded businessFri, 21 Mar 2025 00:00:00 GMT<p>This week I made a decision to ensure Prefect's long-term success by reorganizing our company to operate as a profitable, customer-funded business.</p> <p>This decision had a terrible consequence, and I deeply regret that it meant parting ways with twenty extraordinary colleagues. These are people I personally recruited, who believed in our vision, who said "yes" to this journey. They made Prefect what it is today, and I'm committed to supporting each of them during this transition and will advocate for them all as they move forward.</p> <p>So why take such a difficult action?</p> <p>In uncertain times, we must “control what we can control.” When capital is expensive, investor dependency becomes an existential threat, and on its previous trajectory, Prefect would have required new capital later in 2026.</p> <p>Becoming a profitable business frees us from that constraint. A startup can do extraordinary things when it doesn't operate under the shadow of its next fundraise. Our decisions can now flow purely from what creates value for our customers, not from what extends our timeline.</p> <p>This is a fundamentally different Prefect, built to last and ready to support our growing community. For our users and customers, this change means faster innovation cycles and a partner they can count on regardless of market conditions.</p> <p>I believe this is what a modern startup should look like in 2025: resilient, independent, and focused entirely on creating value for its users. That's the Prefect we're building today.</p> Disabling Tailwind Hover Styles on Mobilehttps://jlowin.dev/blog/disable-tailwind-hover-styles/https://jlowin.dev/blog/disable-tailwind-hover-styles/Now you see me...Mon, 30 Sep 2024 00:00:00 GMT<p>Recently, I added hovering tooltips to the list of blog posts, but I really didn't like how they flashed for a moment when you tapped a link on mobile.</p> <p>It turns out there's an (apparently undocumented) Tailwind feature that allows you to enable hover styles only on supported devices, so I'm making this post as a public service.</p> <p>In <code>tailwind.config.js</code>, you can add the following:</p> <pre><code>export default { future: { hoverOnlyWhenSupported: true, }, } </code></pre> <p>This will only show hover styles (<code>hover:*</code>, <code>group-hover:*</code>, etc.) if the user's device has proper hover support.</p> Curation is the New Discoveryhttps://jlowin.dev/blog/curation-is-the-new-discovery/https://jlowin.dev/blog/curation-is-the-new-discovery/API modesty for the agentic era.Sat, 21 Jun 2025 00:00:00 GMT<p>A common question I hear when teams start building for AI is, "Why can't agents just use our REST API instead of MCP?"</p> <p>My response is usually the same: "Why can't you just use your REST API instead of a UI?" We build interfaces to optimize the experience for the consumer. For the new class of <strong><a href="/blog/as-an-agent-the-new-user-story">"agent stories,"</a></strong> that UI is an MCP server, and designing for it requires a new discipline.</p> <p>Consider a simple change: adding a new filter to an API.</p> <p>For a <strong>human-centric API</strong>, you add an optional parameter with a good default and clean documentation. This is a win. For developers who don't need the new filter, the cognitive surface area is unchanged—they can ignore it. It's discoverable, not intrusive.</p> <p>For an <strong>agent-centric API</strong>, this same "improvement" is a disaster. That new parameter is now part of <em>every</em> interaction. Its instructions will be weighed against all others. Its scope will be (mis)understood. It increases the complexity and token cost of every call. The interaction surface area hasn't been slightly expanded; it has exploded.</p> <p>A human developer is great at ignoring what's irrelevant. An agent must process everything.</p> <p>This is the core challenge. For human APIs, we prioritize <strong>discovery</strong>. For agent APIs, we must prioritize <strong>curation</strong>.</p> <p>MCP is a step in the right direction because it forces us to curate what we expose. But its native discovery—listing all available tools—is still too broad. That's why in <a href="https://gofastmcp.com">FastMCP</a>, we're building dynamic solutions that go far beyond simple filtering, including semantic search and per-session tool visibility. The goal is to let an agent request a narrow, context-aware set of tools, not the entire toolbox.</p> <p>When designing for agents, don't ask "How much can I show them?" Ask "What is the absolute minimum they need?" Your agent stories—and your token bill—will thank you for it.</p> "As an Agent...": The New User Storyhttps://jlowin.dev/blog/as-an-agent-the-new-user-story/https://jlowin.dev/blog/as-an-agent-the-new-user-story/Scrumthing's gotta giveFri, 20 Jun 2025 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>As the author of <a href="https://gofastmcp.com">FastMCP</a>, it might seem strange that I haven’t prioritized an MCP server for <a href="https://www.prefect.io">Prefect</a>. But honestly, the user story for "chatting with your orchestrator" has always felt weak. Developers don't want to have a conversation about their workflows; they want to build, run, and observe them with speed and precision.</p> <p>It turns out I was thinking about the wrong user.</p> <p>My thinking on this shifted after talking with some of our most sophisticated customers about how they debug modern applications. An alert from one system is rarely the end of the story; it's the first domino. And Prefect is the universal pane of glass that provides the first clue in that complex, cross-system investigation. As one customer put it, <em>"A lot of problems we learn about from Prefect are not, in fact, due to Prefect at all."</em> More frequently, a failed process in the orchestrator points to a flow run ID, which helps find an evicted pod in Kubernetes, which leads to a memory spike in a monitoring tool, which finally uncovers an error in an application log.</p> <p>A human <em>can</em> perform this needle-in-a-haystack work. But increasingly, an agent <em>does</em>. The value wasn't in creating a new interface for a human, but in unblocking one for a machine.</p> <p>In an AI-native stack, the protagonist is often no longer a person, but an autonomous agent negotiating APIs, ingesting signals, and chaining tools to deliver value. This requires a fundamental shift from writing "user stories" to defining <strong>"agent stories."</strong> The template for this new artifact is simple but powerful:</p> <blockquote> <p>As an agent, given <code>{context}</code>, I use <code>{tools}</code> to achieve <code>{outcome}</code> with minimal human latency.</p> </blockquote> <p>This reframing forces us to design for a different set of needs. Agents don't care about intuitive UIs or clever microcopy. They care about clear contracts, machine-parsable errors, composability, and minimizing the latency between their actions.</p> <p>This shift helps explain the rapid evolution of AI-native APIs:</p> <p><strong>Phase 1: The Wrapper.</strong> The first wave of MCP servers simply regurgitated existing APIs. This "chat with your API" approach was rightly met with skepticism from savvy teams who understood that a great user experience is more than a conversational veneer over a clunky backend.</p> <p><strong>Phase 2: The Curator.</strong> We are in this phase now. The best teams realize they must consciously <em>design</em> for the LLM. This is an act of curation—thoughtfully reducing scope, renaming cryptic arguments, and adding instructions that guide the agent toward the desired outcome. It’s about tailoring the tool to the new user.</p> <p><strong>Phase 3: The Ecosystem.</strong> This is the frontier. Agent workflows are inherently multi-system. The goal is no longer to build a single, monolithic tool, but to offer a composable node in a larger, automated graph. Your product’s success is measured by how well it interoperates and enables agents to chain actions across a diverse ecosystem.</p> <p>As we design the next generation of software, we must build for two primary personas: our human users and the autonomous agents they deploy. In a growing number of cases, the agent is the more important one. The critical question in product design is shifting from "What does the user want to do?" to "What does the agent need to achieve?"—and that requires a new kind of answer: an <strong>agent story.</strong></p> <p>&lt;Callout color='green'&gt; Want to read more? The next post on agentic product design is <a href="/blog/curation-is-the-new-discovery">Curation is the New Discovery</a> &lt;/Callout&gt;</p> Total Recall: ControlFlow v0.10https://jlowin.dev/blog/controlflow-0-10-total-recall/https://jlowin.dev/blog/controlflow-0-10-total-recall/Simple, persistent memory for AI agentsFri, 27 Sep 2024 00:00:00 GMT<p>I'm really excited about the release of <a href="https://controlflow.ai">ControlFlow v0.10</a>, which introduces a significant new feature: <strong>a flexible and practical memory system for AI agents.</strong></p> <p>Here's an example that shows how simple it is to get started:</p> <pre><code>import controlflow as cf memory = cf.Memory(key="prefs", instructions="Remember user preferences.") cf.run( "Get the user's favorite color", interactive=True, memories=[memory], ) </code></pre> <p>Memory addresses a key limitation in many AI workflows: the inability to leverage knowledge and context beyond the current interaction. Now your agents can:</p> <ul> <li> <p><strong>Recall configuration and project details</strong>: Agents remember settings, project structures, and workflows, ensuring consistency across tasks without repeated setup.</p> </li> <li> <p><strong>Track ongoing issues and resolutions</strong>: Keep a log of common problems and their solutions, enabling agents to offer quick fixes for recurring issues.</p> </li> <li> <p><strong>Integrate past conversations and decisions</strong>: Retain key insights and decisions from previous discussions, informing future tasks without rehashing old ground.</p> </li> <li> <p><strong>Maintain technical styles and best practices</strong>: Agents adapt to coding styles, design patterns, and best practices, applying them consistently across workflows.</p> </li> <li> <p><strong>Store repository knowledge and code locations</strong>: Agents remember where key components or documentation live, speeding up development and debugging.</p> </li> <li> <p><strong>Optimize API usage</strong>: Recall specific API tips, tricks, and edge cases, providing more efficient solutions beyond the standard documentation.</p> </li> <li> <p><strong>Summarize long-term project insights</strong>: Capture key learnings from long-running projects, enabling agents to seamlessly continue tasks without re-creating context.</p> </li> </ul> <p>An emergent characterization of production AI workflows is that they tend to be short, directed, and scaled across many concurrent agents. In other words, effective memory systems shouldn't optimize for summarizing the longest possible conversation (as this is straightforward within any single session), but for rapidly establishing the context that ensures consistent behavior across many invocations.</p> <p>ControlFlow's solution is a modular, vector-backed memory system that not only allows you to store and retrieve information, but lets you quickly provide access to various memories on a per-task or per-agent basis.</p> <h2>Getting Started with Memory</h2> <p>Suppose we want to store user preferences, but we need to make sure that Alice's preferences don't get mixed up with Bob's. To achieve this, we can give each user their own dedicated memory module. Here's a flow that demonstrates how to do it:</p> <pre><code>import controlflow as cf @cf.flow def demo(user_id: str): # create a memory module for the user memory = cf.Memory( key=f"{user_id}_prefs", instructions="Remember user preferences about writing.", ) # use the memory module in the task return cf.run( "Write a poem on a topic of the user's choice", instructions="Share drafts with the user until they are happy.", interactive=True, memories=[memory], ) # run the flow for Alice and Bob, without mixing up their memories demo("Alice") demo("Bob") </code></pre> <p>Later, Alice and Bob might be part of the same conversation, and we could provide both of their memory modules to the agent at the same time!</p> <p>To start using the new memory feature, upgrade to ControlFlow 0.10 and <a href="https://controlflow.ai/patterns/memory#provider">install</a> your preferred vector store (ControlFlow currently supports <a href="https://trychroma.com/">Chroma</a> and <a href="https://lancedb.com/">LanceDB</a>).</p> <pre><code>pip install --upgrade controlflow </code></pre> <p>For more information on integrating memory into your workflows, please refer to the <a href="https://controlflow.ai/patterns/memory">updated documentation</a>.</p> <p>Happy AI engineering!</p> FastMCP 3.0 Beta 2https://jlowin.dev/blog/fastmcp-3-beta-2/https://jlowin.dev/blog/fastmcp-3-beta-2/2 Fast 2 BetaSun, 08 Feb 2026 12:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>The <a href="/blog/fastmcp-3">FastMCP 3 beta</a> has been running for a few weeks now, and we've been busy. The architecture (providers, transforms, components) has held up well under real-world usage, which is exactly what a beta period is for. But we haven't been sitting around waiting for bug reports.</p> <p>Beta 2 ships a substantial batch of new features that round out the framework for a stable release. The biggest theme is the CLI: FastMCP can now discover, query, and invoke tools on any MCP server from the terminal, and even generate standalone CLI scripts from tool schemas. We've also landed CIMD (the DCR replacement), protocol-level MCP Apps support, response size limiting, and background task elicitation.</p> <p><em>For a refresher on the beta 1 architecture and features, see the <a href="/blog/fastmcp-3-whats-new">What's New in FastMCP 3.0</a> post.</em></p> <p>&lt;Callout color="green"&gt;</p> <h3>Get Started</h3> <pre><code>pip install fastmcp==3.0.0b2 </code></pre> <p>&lt;/Callout&gt;</p> <h2>The CLI</h2> <p>Four new commands that collectively make <code>fastmcp</code> useful for a lot more than running servers.</p> <h3><code>fastmcp list</code> and <code>fastmcp call</code></h3> <p>You can now query and invoke tools on any MCP server directly from the terminal. Remote URLs, local Python files, MCPConfig JSON files, arbitrary stdio commands. Anything.</p> <pre><code># Discover tools on a server fastmcp list http://localhost:8000/mcp fastmcp list server.py # Call a tool fastmcp call server.py greet name=World fastmcp call http://localhost:8000/mcp search query=hello limit=5 </code></pre> <p>Tool arguments are auto-coerced using the tool's JSON schema, so <code>limit=5</code> becomes an integer automatically. JSON objects work as positional args. OAuth fires automatically for HTTP targets that require it.</p> <p>This alone is transformative for development. Instead of wiring up a client to test your server, you just call it. But the real power comes when you combine it with discovery.</p> <h3><code>fastmcp discover</code></h3> <p><code>fastmcp discover</code> scans your editor configs (Claude Desktop, Claude Code, Cursor, Gemini CLI, Goose) and project-level <code>mcp.json</code> files for MCP server definitions. Once discovered, you reference servers by name:</p> <pre><code># See all configured servers across your editors fastmcp discover # Use a server by name fastmcp list weather fastmcp call weather get_forecast city=London # Disambiguate with source:name fastmcp call cursor:weather get_forecast city=London </code></pre> <p>Every server you've configured in any editor is now one command away, regardless of transport.</p> <p>The repo also ships a <a href="https://github.com/jlowin/fastmcp/blob/main/skills/fastmcp-client-cli/SKILL.md">CLI skill</a> so your coding assistant already knows how to use these commands. Drop it into your agent's skills directory and it can discover, list, and call MCP tools on your behalf.</p> <h3><code>fastmcp generate-cli</code></h3> <p>This is the feature that made me grin when I first saw it working. <code>fastmcp generate-cli</code> connects to any MCP server, reads its tool schemas, and writes a standalone Python CLI script where every tool becomes a typed subcommand with flags, help text, and tab completion.</p> <pre><code># Generate from any server fastmcp generate-cli weather my_weather_cli.py # Use the generated script python my_weather_cli.py call-tool get_forecast --city London --days 3 python my_weather_cli.py list-tools </code></pre> <p>The insight: MCP tool schemas already contain everything a CLI framework needs. Parameter names, types, descriptions, required/optional status. The generator maps JSON Schema directly into <a href="https://cyclopts.readthedocs.io/">cyclopts</a> commands. The generated script embeds the resolved transport, so it's self-contained. Users don't need to know about MCP or FastMCP to use it.</p> <p>This is how MCP tools escape the chatbot. Your agent's tools become anyone's CLI.</p> <h2>MCP Apps</h2> <p><a href="https://modelcontextprotocol.io/specification/2025-06-18/server/apps">MCP Apps</a> is the spec extension that lets MCP servers deliver interactive UIs via sandboxed iframes. Beta 2 adds SDK-level support: extension negotiation, typed UI metadata on tools and resources, and the <code>ui://</code> resource scheme.</p> <pre><code>from fastmcp import FastMCP from fastmcp.server.apps import ToolUI, ResourceUI mcp = FastMCP("My Server") # Register a UI bundle as a resource @mcp.resource("ui://dashboard/view.html") def dashboard_html() -&gt; str: return Path("./dist/index.html").read_text() # Tool with a UI — clients render an iframe alongside the result @mcp.tool(ui=ToolUI(resource_uri="ui://dashboard/view.html")) async def list_users() -&gt; list[dict]: return [{"id": "1", "name": "Alice"}] # App-only tool — visible to the UI but hidden from the model @mcp.tool(ui=ToolUI( resource_uri="ui://dashboard/view.html", visibility=["app"] )) async def delete_user(id: str) -&gt; dict: return {"deleted": True} </code></pre> <p>This is the foundation. We're shipping the protocol-level support (CSP, permissions, extension negotiation) so that when MCP clients start rendering apps, FastMCP servers are ready. The higher-level component DSL, the in-repo renderer, and the <code>FastMCPApp</code> class are coming in future betas.</p> <p>Tools can detect whether the connected client supports apps at runtime via <code>ctx.client_supports_extension()</code>, which means you can serve rich structured data to app-capable clients and fall back to text for everyone else.</p> <h2>CIMD: Client Authentication Without DCR</h2> <p>CIMD (Client ID Metadata Documents) replaces Dynamic Client Registration for OAuth-authenticated MCP servers. Instead of clients registering dynamically with each server via a POST endpoint, they host a static JSON document at an HTTPS URL. That URL becomes the client's <code>client_id</code>, and servers verify identity through domain ownership.</p> <pre><code>from fastmcp import Client from fastmcp.client.auth import OAuth async with Client( "https://mcp-server.example.com/mcp", auth=OAuth( client_metadata_url="https://myapp.example.com/oauth/client.json", ), ) as client: await client.ping() </code></pre> <p>We also ship CLI tools for generating and validating CIMD documents:</p> <pre><code># Generate a CIMD document fastmcp auth cimd create --name "My App" \ --redirect-uri "http://localhost:*/callback" \ --client-id "https://myapp.example.com/oauth/client.json" # Validate a hosted document fastmcp auth cimd validate https://myapp.example.com/oauth/client.json </code></pre> <p>CIMD is enabled by default on <code>OAuthProxy</code> and all its provider subclasses (GitHub, Google, Azure, etc.). The server-side implementation includes SSRF-hardened document fetching with DNS pinning, dual redirect URI validation, HTTP cache-aware revalidation, and <code>private_key_jwt</code> assertion support.</p> <h2>ResponseLimitingMiddleware</h2> <p>Context window protection, built in. This middleware controls tool response sizes, preventing large outputs from overwhelming LLM context windows.</p> <pre><code>from fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware # Limit all tool responses to 500KB mcp.add_middleware(ResponseLimitingMiddleware(max_size=500_000)) # Limit only specific tools mcp.add_middleware(ResponseLimitingMiddleware( max_size=100_000, tools=["search", "fetch_data"], )) </code></pre> <p>Text responses are truncated at UTF-8 character boundaries. Structured responses (tools with <code>output_schema</code>) raise <code>ToolError</code> since truncation would corrupt the schema. Size metadata gets added to the result's <code>meta</code> field for monitoring. This is the kind of production guard rail that saves you from a tool that returns a 10MB JSON blob and blows out your token budget.</p> <h2>Background Task Elicitation</h2> <p><code>Context</code> now works transparently in background tasks running in Docket workers. Previously, tools running as background tasks couldn't use <code>ctx.elicit()</code> because there was no active request context. Now, when a tool executes in a Docket worker, <code>Context</code> detects this and routes elicitation through Redis-based coordination: the task sets its status to <code>input_required</code>, sends a notification, and waits for the client to respond.</p> <pre><code>@mcp.tool(task=True) async def interactive_task(ctx: Context) -&gt; str: # Works transparently in both foreground and background result = await ctx.elicit("Please provide additional input", str) if isinstance(result, AcceptedElicitation): return f"You provided: {result.data}" return "Elicitation was declined" </code></pre> <p><code>ctx.is_background_task</code> and <code>ctx.task_id</code> are available for tools that need to branch on execution mode.</p> <h2>Everything Else</h2> <p>A few more things that shipped in beta 2:</p> <ul> <li><strong><code>fastmcp install goose</code></strong>: Generates a Goose deeplink URL and opens it, installing your server as a STDIO extension. Goose requires <code>uvx</code> rather than <code>uv run</code>, and the command handles the difference automatically.</li> <li><strong><code>fastmcp install stdio</code></strong>: Generates full <code>uv run</code> commands for running FastMCP servers over stdio, making it easy to integrate with MCP clients that need a command string.</li> <li><strong>Expanded reload file watching</strong>: The <code>--reload</code> flag now watches JavaScript, TypeScript, HTML, CSS, config files, and media assets. Necessary for MCP Apps with frontend bundles.</li> <li><strong><code>require_auth</code> removed</strong>: Since configuring an <code>AuthProvider</code> already rejects unauthenticated requests at the transport level, <code>require_auth</code> was redundant. Use <code>require_scopes</code> instead.</li> </ul> <p>Beyond features, this release includes a wave of bug fixes and stability improvements across OAuth, transports, task execution, and the CLI. Seven new contributors joined FastMCP in this release alone, which is extraordinary for a beta. Full details in the <a href="https://github.com/jlowin/fastmcp/releases/tag/v3.0.0b2">release notes</a>.</p> <hr /> <p>We're getting close. The architecture is stable, the feature set is filling out, and the beta feedback has been exactly what we needed. If you've been waiting for the right time to try FastMCP 3, this is it.</p> <p>Happy (context) engineering!</p> <p>&lt;Callout color="gray"&gt;</p> <h3>About This Beta</h3> <p><strong>Install:</strong> <code>pip install fastmcp==3.0.0b2</code></p> <ul> <li><strong>Beta 1 Features:</strong> <a href="/blog/fastmcp-3-whats-new">What's New in FastMCP 3.0</a></li> <li><strong>Full Documentation:</strong> <a href="https://gofastmcp.com">gofastmcp.com</a></li> <li><strong>GitHub:</strong> <a href="https://github.com/jlowin/fastmcp">github.com/jlowin/fastmcp</a> &lt;/Callout&gt;</li> </ul> FastMCP 3.0 is GAhttps://jlowin.dev/blog/fastmcp-3-launch/https://jlowin.dev/blog/fastmcp-3-launch/Three at lastWed, 18 Feb 2026 12:00:00 GMT<p>FastMCP 3.0 is stable and generally available.</p> <pre><code>pip install fastmcp -U </code></pre> <p>It's been about a month since the first beta. We shipped two betas and two release candidates, landed code from 21 new contributors, and saw over 100,000 opt-in pre-release installs — extraordinary for a beta that requires explicit version pinning. We wrote three complete upgrade guides and an LLM migration prompt you can paste into your coding assistant to automate the transition. The architecture held up. The upgrade path is smooth. We're ready.</p> <p>This is also the release where FastMCP moves from <code>jlowin/fastmcp</code> to <code>PrefectHQ/fastmcp</code>. When I built this over a weekend in late 2024, it lived under my personal account because it was a side project. That stopped being accurate a long time ago. FastMCP has the full engineering support of the Prefect team and is a core pillar of our <a href="https://prefect.io/horizon">Horizon</a> platform. Special thanks to <a href="https://linkedin.com/in/williamseaston">Bill Easton</a>, FastMCP's first external maintainer, whose fingerprints are all over the transform architecture that makes 3.0 tick. For you, nothing changes — GitHub forwards all links, PyPI is the same, imports are the same. A major version felt like the right moment to make the move official.</p> <p>We also built three separate upgrade guides, because people are coming from different places: <a href="https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2">from FastMCP 2</a>, <a href="https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk">from the MCP SDK</a>, and <a href="https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk">from the low-level SDK</a>. All three include a copyable LLM prompt to help you automate your migration. Early feedback from the community suggests most upgrades are straightforward, and for many users it should just work.</p> <h2>What's New</h2> <p>This is, by a wide margin, the largest release in FastMCP's history. Several of the features below could have shipped as standalone packages. They didn't, because they all flow from a single architectural redesign that makes FastMCP ready for the next generation of MCP.</p> <p>The surface API is largely unchanged — <code>@mcp.tool()</code> still works exactly as before. What changed is everything underneath. So much effort went into making FastMCP 3 extensible, observable, debuggable, and performant that if we did our jobs right, you'll barely notice the architecture. You'll just notice that everything works better and a lot more is possible.</p> <p>Here's what you can do now.</p> <h3>Build servers from anything</h3> <p>Your components no longer have to live in one file with one server. Point a <code>FileSystemProvider</code> at a directory and it discovers your tools automatically, with hot reload. Wrap a REST API with <code>OpenAPIProvider</code>. Proxy a remote MCP server. Deliver agent skills as MCP resources. Write your own provider for whatever source makes sense. Compose multiple providers into one server, share one across many, or chain them with transforms that rename, namespace, filter, version, and secure components as they flow to clients. <code>ResourcesAsTools</code> and <code>PromptsAsTools</code> expose non-tool components to tool-only clients.</p> <h3>Use FastMCP as a CLI</h3> <p>FastMCP is now a developer tool, not just a framework. <code>fastmcp list</code> and <code>fastmcp call</code> let you query and invoke tools on any MCP server from your terminal — remote URLs, local files, stdio commands. <code>fastmcp discover</code> scans your editor configs (Claude Desktop, Cursor, Goose, Gemini CLI) and finds all your configured servers by name. <code>fastmcp generate-cli</code> reads a server's schemas and writes a standalone typed CLI where every tool is a subcommand with flags and help text. <code>fastmcp install</code> registers your server with Claude Desktop, Cursor, or Goose in one command.</p> <h3>Ship to production</h3> <p>Component versioning: serve <code>@tool(version="2.0")</code> alongside older versions from one codebase. Granular authorization on individual components, async auth checks that can hit databases or external services, and server-wide policies via <code>AuthMiddleware</code>. OAuth gets CIMD, Static Client Registration, Azure OBO via dependency injection, JWT audience validation, and confused-deputy protections. Native OpenTelemetry tracing with MCP semantic conventions. Response size limiting. Background tasks via Docket with distributed Redis notification and <code>ctx.elicit()</code> relay. Security fixes include dropping <code>diskcache</code> (CVE-2025-69872) and upgrading <code>python-multipart</code> and <code>protobuf</code> for additional CVEs.</p> <h3>Develop faster</h3> <p><code>--reload</code> auto-restarts on file changes. Decorated functions stay callable — import them, call them, unit test them like normal Python. Sync tools auto-dispatch to a threadpool so they don't block the event loop. Tool timeouts. MCP-compliant pagination. Composable lifespans. <code>PingMiddleware</code> for keepalive. Concurrent tool execution when the LLM returns multiple calls in one response.</p> <h3>Adapt per session</h3> <p>Session state persists across requests via <code>ctx.set_state()</code> / <code>ctx.get_state()</code>. <code>ctx.enable_components()</code> and <code>ctx.disable_components()</code> let servers adapt dynamically per client — show admin tools only after authentication, progressively reveal capabilities, or scope access by role. Chain these together and you get playbooks: dynamic MCP-native workflows that guide agents through processes instead of dumping everything into the context window at once.</p> <h3>Build apps (3.1 preview)</h3> <p>Spec-level support for MCP Apps is already in: <code>ui://</code> resource scheme, typed UI metadata, extension negotiation, and runtime detection. Full apps support — including a Python DSL for building generative UIs without writing JavaScript — lands in 3.1.</p> <p>For the complete feature guide: <a href="/blog/fastmcp-3">Introducing FastMCP 3.0</a> and <a href="/blog/fastmcp-3-beta-2">Beta 2</a>.</p> <h2>One Honest Disclaimer</h2> <p>We know a lot of people are about to encounter FastMCP 3 for the first time — not because they chose to upgrade, but because they didn't pin their dependencies. If that's you, and something breaks: we're sorry, and the upgrade guides will get you sorted quickly. We did everything we could to minimize breaking changes, but a major version is a major version. If you encounter any bugs, please <a href="https://github.com/PrefectHQ/fastmcp/issues/new">open an issue</a>.</p> <p>If you maintain a framework that depends on FastMCP 2.x, please pin your dependency. We want everyone on 3.0 as fast as possible, but we want them there on purpose.</p> <h2>Get Started</h2> <pre><code>pip install fastmcp -U </code></pre> <ul> <li><a href="/blog/fastmcp-3-whats-new">What's New in FastMCP 3.0</a> — the complete feature guide</li> <li><a href="https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2">Upgrade Guides</a> — from FastMCP 2, the MCP SDK, or the low-level SDK</li> <li><a href="https://github.com/PrefectHQ/fastmcp">GitHub</a> — the new home</li> <li><a href="https://gofastmcp.com">Documentation</a></li> </ul> Stop Calling Tools, Start Writing Code (Mode)https://jlowin.dev/blog/fastmcp-3-1-code-mode/https://jlowin.dev/blog/fastmcp-3-1-code-mode/Code to JoyTue, 03 Mar 2026 00:00:00 GMT<p>import Callout from "@components/blog/Callout.astro";</p> <p>MCP servers scale in a way that punishes success.</p> <p>A server with ten tools works beautifully. The LLM sees all ten schemas, picks the right one, calls it. A server with two hundred tools dumps two hundred schemas into the context window before the LLM reads a single word of the user's request: tens of thousands of tokens, most of them irrelevant.</p> <p>The execution model compounds the problem. Every tool call is a round-trip. The LLM calls a tool, the result passes back through the context window, the LLM reasons about it, calls another tool. Intermediate results that only exist to feed the next step burn tokens flowing through the model on every turn.</p> <p>The code mode pattern, <a href="https://blog.cloudflare.com/code-mode/">introduced by Cloudflare</a> and <a href="https://www.anthropic.com/engineering/code-execution-with-mcp">explored by Anthropic</a>, addresses both problems at once: instead of calling tools one at a time, the LLM writes a script that composes them. Search for what's available, write code, execute it in a sandbox. The intermediate results stay inside the sandbox. The context window stays clean. Cloudflare recently <a href="https://blog.cloudflare.com/code-mode-mcp/">shipped a server-side implementation</a> for their own API: two tools covering 2,500 endpoints in roughly 1,000 tokens.</p> <p>FastMCP 3.1 ships server-side code mode with fully configurable discovery, and the server-side part matters more than it sounds.</p> <h2>CodeMode</h2> <p>Here's a normal FastMCP server with <strong>CodeMode</strong> applied:</p> <pre><code>from fastmcp import FastMCP from fastmcp.experimental.transforms.code_mode import CodeMode mcp = FastMCP("Server", transforms=[CodeMode()]) @mcp.tool def add(x: int, y: int) -&gt; int: """Add two numbers.""" return x + y @mcp.tool def multiply(x: int, y: int) -&gt; int: """Multiply two numbers.""" return x * y </code></pre> <p>The only difference from a standard server is <code>transforms=[CodeMode()]</code>. The tool functions stay the same. But clients connecting to this server no longer see <code>add</code> and <code>multiply</code> directly; they see the meta-tools that CodeMode provides: tools for discovering what's available and for writing code that calls them.</p> <p>The default flow has three stages. Granted, three stages might sound like a lot for something intended to <em>reduce</em> server round-trips. The original code mode pattern, introduced by Cloudflare, had no discovery phase at all: clients loaded every tool definition into context, then executed code against them. This solved the sequential calling problem but not the context bloat problem. Anthropic introduced a two-stage approach: search for relevant tools, then execute. This addressed both problems.</p> <p>For servers complex enough to need code mode, we've found that an additional stage makes a meaningful difference. Separating search from schema retrieval lets the search tool stay lightweight, returning only names and brief descriptions, while a dedicated schema step provides the precision the LLM needs to write correct code. But if you want something else, FastMCP permits full customization of this flow to have as few or as many stages as you need.</p> <p>Here's how the three default stages play out with the server above:</p> <p>First, the LLM searches. It calls <code>search(query="math numbers")</code> and gets back tool names and descriptions: a lightweight index. Instead of loading two hundred schemas, it sees a few lines of text about the tools that match.</p> <p>Next, it requests parameter details for the tools it found. <code>get_schema(tools=["add", "multiply"])</code> returns parameter names, types, and required markers. Not the full JSON schema (by default), but enough to write code against.</p> <p>Finally, it writes a Python script and executes it in a sandbox:</p> <pre><code>a = await call_tool("add", {"x": 3, "y": 4}) b = await call_tool("multiply", {"x": a, "y": 2}) return b </code></pre> <p>Three round-trips: search, schema, execute. The intermediate result (<code>a</code>) never enters the context window. <code>call_tool</code> is the only function available inside the sandbox; no filesystem, no network, just tool calls and Python.</p> <h2>Discovery</h2> <p>The three-stage flow is the default. CodeMode's discovery surface is fully configurable, because different tool catalogs need different approaches.</p> <p>CodeMode ships four discovery tools. All of them share a tunable <strong>detail level</strong> that controls how much information each response includes:</p> <table> <thead> <tr> <th>Level</th> <th>Output</th> <th>Token cost</th> </tr> </thead> <tbody> <tr> <td><code>"brief"</code></td> <td>Tool names and one-line descriptions</td> <td>Cheapest</td> </tr> <tr> <td><code>"detailed"</code></td> <td>Compact markdown with parameter names, types, and required markers</td> <td>Medium</td> </tr> <tr> <td><code>"full"</code></td> <td>Complete JSON Schema</td> <td>Most expensive</td> </tr> </tbody> </table> <p>This is significant. Even <strong>ListTools</strong>, which dumps the entire catalog, can produce substantially fewer tokens than a standard MCP handshake when set to <code>"brief"</code> or <code>"detailed"</code>. A standard <code>tools/list</code> response includes the full JSON Schema for every tool: argument names, types, nested objects, descriptions, constraints. ListTools at <code>"brief"</code> returns just names and descriptions. The context dump tax is still there, but it's a fraction of what it would be, and the sequential calling tax is eliminated entirely because tool calls happen inside the sandbox.</p> <p>By default, two discovery tools are enabled:</p> <p><strong>Search</strong> finds tools by natural-language query using BM25 ranking. Defaults to <code>"brief"</code> detail. The LLM can override the detail level per call, requesting <code>"detailed"</code> for inline schemas or <code>"full"</code> for the complete JSON Schema.</p> <p><strong>GetSchemas</strong> takes a list of tool names and returns parameter details. Defaults to <code>"detailed"</code>. The fallback for when search results aren't enough to write code against.</p> <p>Two more are opt-in:</p> <p><strong>ListTools</strong> dumps the entire catalog. At <code>"brief"</code> detail, this is a lightweight alternative to standard MCP tool listing. For small servers, under twenty tools or so, seeing everything upfront can be faster than searching.</p> <p><strong>GetTags</strong> lets the LLM browse tools by <a href="https://gofastmcp.com/servers/tools#tags">tag</a> metadata, then pass tags into Search to narrow results. Useful when tools have a natural taxonomy.</p> <p>The discovery configuration is where the server author's knowledge becomes design. A large platform server might use all four tools with progressive detail levels: tags for orientation, search for narrowing, schemas for precision. A smaller server can collapse to two stages by bumping search detail:</p> <pre><code>from fastmcp.experimental.transforms.code_mode import CodeMode, Search, GetSchemas code_mode = CodeMode( discovery_tools=[Search(default_detail="detailed"), GetSchemas()], ) </code></pre> <p>Now search returns parameter schemas inline, and the LLM goes straight from search to execute. GetSchemas stays available as a fallback for complex parameter trees.</p> <p>This two-stage configuration is exactly the pattern Cloudflare <a href="https://blog.cloudflare.com/code-mode-mcp/">shipped for their API</a>: search returns enough detail to write code, execute runs it. In FastMCP, it's one line applied to any server. Cloudflare's results — and early usage patterns — suggest two-stage may be the better default for most servers. It's something we're actively evaluating.</p> <p>A very simple server can skip discovery entirely and bake tool instructions into the execute tool's description:</p> <pre><code>code_mode = CodeMode( discovery_tools=[], execute_description=( "Available tools:\n" "- add(x: int, y: int) -&gt; int: Add two numbers\n" "- multiply(x: int, y: int) -&gt; int: Multiply two numbers\n\n" "Write Python using `await call_tool(name, params)` and `return` the result." ), ) </code></pre> <p>Each of these patterns is a conscious choice about the tradeoff between token cost and discovery accuracy. The server author makes that choice once, and every client benefits. This is the fundamental advantage of server-side code mode: the person who knows the tools best is the one deciding how they're discovered and composed.</p> <h2>Composition</h2> <p>In the <a href="/blog/fastmcp-3">FastMCP 3.0 architecture</a>, components flow through a pipeline. <strong>Providers</strong> source them; <strong>transforms</strong> modify them on the way to clients. A transform can rename, filter, namespace, or reshape what a provider exposes, and transforms compose: stack them, and each one processes the output of the previous.</p> <p>CodeMode is a transform. It works with everything else in the system without special-casing.</p> <p>Apply it to an entire server, or to just one provider. Some tools go through code mode, others stay directly accessible. Chain it with other transforms: add a namespace to a mounted sub-server, then apply CodeMode to the result. Filter tools by tag or version, then wrap whatever passes through.</p> <p>One pattern worth highlighting is to proxy a remote server, then apply <code>CodeMode</code>:</p> <pre><code>from fastmcp.server import create_proxy from fastmcp.experimental.transforms.code_mode import CodeMode remote = create_proxy("https://api.example.com/mcp") remote.add_transform(CodeMode()) remote.run() </code></pre> <p>That remote server now has a code execution interface with tunable discovery. The original authors didn't build one. The person running the proxy configured one that fits their application.</p> <p>The behavior falls out of the architecture.</p> <p>&lt;Callout color="blue"&gt; <strong>Coming soon:</strong> We're adding configurable code mode for every server hosted on <a href="https://prefect.io/horizon">Prefect Horizon</a>. No code changes required. &lt;/Callout&gt;</p> <h2>The Sandbox</h2> <p>The Python execution environment is sandboxed via Pydantic's <a href="https://github.com/pydantic/monty">Monty</a> project, an experimental Python sandbox that restricts LLM-generated code to <code>call_tool</code> and standard Python. No filesystem access, no network access, nothing outside the sandbox boundary.</p> <p>Building a Python sandbox that's secure enough for production and flexible enough to be useful is genuinely hard. The Pydantic team has been doing excellent work on Monty, and CodeMode wouldn't exist without it.</p> <p>Resource limits are configurable: timeouts, memory caps, recursion depth.</p> <pre><code>from fastmcp.experimental.transforms.code_mode import CodeMode, MontySandboxProvider sandbox = MontySandboxProvider( limits={"max_duration_secs": 10, "max_memory": 50_000_000}, ) mcp = FastMCP("Server", transforms=[CodeMode(sandbox_provider=sandbox)]) </code></pre> <p>The sandbox provider itself is replaceable. Implement the <code>SandboxProvider</code> protocol and point CodeMode at a Docker container, a remote execution service, whatever fits the deployment.</p> <h2>Getting Started</h2> <pre><code>pip install "fastmcp[code-mode]" </code></pre> <p>CodeMode is experimental. The core interface is stable, but the specific discovery tools and their parameters may evolve as we learn more about what works in practice.</p> <p><a href="https://gofastmcp.com/servers/transforms/code-mode">Documentation</a> · <a href="https://github.com/PrefectHQ/fastmcp">GitHub</a></p> <p>Happy (context) engineering!</p>