<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Opula</title>
    <description>The latest articles on DEV Community by Opula (@opula_io).</description>
    <link>https://dev.to/opula_io</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027649%2Ff5e51f97-e179-4dfb-9ce4-079798506446.png</url>
      <title>DEV Community: Opula</title>
      <link>https://dev.to/opula_io</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/opula_io"/>
    <language>en</language>
    <item>
      <title>Your GitHub repo can be a Claude plugin marketplace. Here is the whole setup.</title>
      <dc:creator>Opula</dc:creator>
      <pubDate>Wed, 29 Jul 2026 10:46:43 +0000</pubDate>
      <link>https://dev.to/opula_io/your-github-repo-can-be-a-claude-plugin-marketplace-here-is-the-whole-setup-24cg</link>
      <guid>https://dev.to/opula_io/your-github-repo-can-be-a-claude-plugin-marketplace-here-is-the-whole-setup-24cg</guid>
      <description>&lt;p&gt;I spent an afternoon turning a GitHub repo into a Claude plugin marketplace.&lt;br&gt;
The docs get you 80% there. The last 20% is where I burned two hours, so&lt;br&gt;
here's the setup plus the two gotchas that aren't written down anywhere.&lt;/p&gt;
&lt;h2&gt;
  
  
  The structure
&lt;/h2&gt;

&lt;p&gt;Two files matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.claude-plugin/marketplace.json   &amp;lt;- the catalog
plugins/opula/plugin.json         &amp;lt;- one entry per plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;marketplace.json&lt;/code&gt; lists what's in the repo. Each plugin points to its own&lt;br&gt;
&lt;code&gt;plugin.json&lt;/code&gt;, which declares name, version, and (if it ships an MCP server)&lt;br&gt;
an &lt;code&gt;.mcp.json&lt;/code&gt; pointing at a remote HTTP endpoint. Mine authenticates via&lt;br&gt;
Dynamic Client Registration, so there's no API key to hand out. Anyone who&lt;br&gt;
installs it goes through OAuth on first connect.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installing it
&lt;/h2&gt;

&lt;p&gt;Two lines, from inside Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add evan-moon/opula-plugin
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;opula@opula
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No forking, no local clone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: two version numbers, one sync you can't see
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;plugin.json&lt;/code&gt; and &lt;code&gt;marketplace.json&lt;/code&gt; both carry a version. Bump one and&lt;br&gt;
forget the other, and Claude Code will happily install the stale one with no&lt;br&gt;
error. Worse: claude.ai's marketplace sync is manual on top of that, so a&lt;br&gt;
fix you shipped an hour ago might not reach anyone who installs through the&lt;br&gt;
web client until you go trigger the sync yourself. Claude Code gives you&lt;br&gt;
zero visibility into this step. You just have to remember it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 2: installed but "broken"
&lt;/h2&gt;

&lt;p&gt;If someone installs the plugin but never connects the underlying MCP&lt;br&gt;
server, they'll see the skills load fine and then hit a wall the moment a&lt;br&gt;
skill tries to call a tool. Nothing crashes, nothing errors clearly, it just&lt;br&gt;
looks broken. The fix is a connection step, not a bug fix, but nothing in&lt;br&gt;
the UI tells you that's what's missing. If you're shipping a plugin with an&lt;br&gt;
MCP backend, put the "go connect it" instruction directly in your first&lt;br&gt;
skill's output, not just in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the skills are actually doing
&lt;/h2&gt;

&lt;p&gt;Worth saying out loud: the 6 skills in this plugin aren't a smarter model&lt;br&gt;
bolted on top. They're routing. &lt;code&gt;analyze-holding&lt;/code&gt; walks a fixed catalog of&lt;br&gt;
checks instead of freestyling an analysis. &lt;code&gt;month-end&lt;/code&gt; asks a clarifying&lt;br&gt;
question up front if data's missing instead of guessing. &lt;code&gt;import-ledger&lt;/code&gt;&lt;br&gt;
confirms the column mapping before it writes anything. None of that is&lt;br&gt;
"AI reasoning" in any interesting sense, it's just structure that keeps the&lt;br&gt;
model from improvising in places where improvising is the wrong move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token cost, since nobody publishes this
&lt;/h2&gt;

&lt;p&gt;Rough numbers, not exact: always-on context from the plugin lands around&lt;br&gt;
1k tokens, every turn, whether you use it or not. Per-skill invocation adds&lt;br&gt;
roughly 1.5k-2k depending on which one fires and how much markdown/code&lt;br&gt;
gets pulled in. I'm rounding these on purpose, if you're building one of&lt;br&gt;
these, run &lt;code&gt;/context&lt;/code&gt; in a session with the plugin installed and get your&lt;br&gt;
own real numbers rather than trusting mine.&lt;/p&gt;




&lt;p&gt;This is the plugin for &lt;a href="https://opula.io?utm_source=devto&amp;amp;utm_campaign=plugin-v040&amp;amp;utm_content=setup-guide" rel="noopener noreferrer"&gt;opula&lt;/a&gt;,&lt;br&gt;
a portfolio copilot I built for tracking holdings across multiple brokers.&lt;br&gt;
The plugin adds 6 skills on top of the existing MCP connector; same account,&lt;br&gt;
Pro plan or above. If you hit either gotcha above while setting up your own&lt;br&gt;
marketplace, I'd genuinely like to hear about it.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Can Claude Analyze My Portfolio?</title>
      <dc:creator>Opula</dc:creator>
      <pubDate>Tue, 14 Jul 2026 21:17:06 +0000</pubDate>
      <link>https://dev.to/opula_io/can-claude-analyze-my-portfolio-4h79</link>
      <guid>https://dev.to/opula_io/can-claude-analyze-my-portfolio-4h79</guid>
      <description>&lt;p&gt;If Claude can already search the web, read a 10-K, and explain what a rate cut does to long-duration equities, the fair question is why you would connect anything to it at all. It is the right question, and the honest answer is that for a large class of questions you should not. Raw Claude is enough.&lt;/p&gt;

&lt;p&gt;The gap is narrower and sharper than "Claude does not know finance." Claude knows finance. What it does not know is you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What raw Claude already does well
&lt;/h2&gt;

&lt;p&gt;Be clear about this before the sales pitch, because pretending otherwise would insult anyone who has actually used it.&lt;/p&gt;

&lt;p&gt;Claude with web search will look up a current quote, summarize an earnings call, explain a valuation multiple, walk you through how a Monte Carlo simulation works, and reason about a macro scenario better than most of the commentary you would read instead. If your question is about the world, and not about your own balance sheet, a connector adds nothing. Ask Claude directly.&lt;/p&gt;

&lt;p&gt;The trouble starts the moment the answer depends on what you actually own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things that break when the question is about your money
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. It starts from zero every time
&lt;/h3&gt;

&lt;p&gt;A chat has no memory of your holdings. You can paste them in, and many people do, and it works for exactly one conversation. There is no cost basis, no purchase date, no daily snapshot series behind it. So "how concentrated am I really", "what is my realized gain this year", and "how correlated are my top five positions over the last 90 days" are not questions it can answer. It can only answer them about the numbers you re-typed, this once, from memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The same question gives a different answer twice
&lt;/h3&gt;

&lt;p&gt;LLM inference is not deterministic, and it is not deterministic even at temperature zero. Thinking Machines Lab traced the cause to batch-invariance in inference kernels: the batch your request lands in varies with server load, so the arithmetic varies with it. They fixed it in a research setting and got 1,000 bitwise-identical runs, which tells you how much engineering it takes to get the property most people assume is free.&lt;/p&gt;

&lt;p&gt;For an essay, drift between two runs is invisible. For "what percent of my net worth is in one stock", it is the whole point. A number you cannot reproduce is not a number, it is an opinion with decimals.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It estimates where it should compute
&lt;/h3&gt;

&lt;p&gt;Ask a model for a Sharpe ratio and it will give you one. Whether it computed it or produced a plausible-looking float is not visible from the output. Research on financial-domain LLMs finds they frequently get figures wrong when they are not grounded in a database, and that grounding cuts financial hallucination substantially.&lt;/p&gt;

&lt;p&gt;This is not a reason to distrust Claude. It is a reason not to ask it to be a calculator. Judgment is what a model is good at. Arithmetic over your own ledger is what a rule should do.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. It cannot see the assets that are not on the web
&lt;/h3&gt;

&lt;p&gt;Web search finds what is published. Your jeonse deposit is not published. Neither is the gold bar you bought at a Korean bank, the apartment you paid off, or the stake you hold in a private company. No amount of searching surfaces them, because they are not missing from the internet, they are simply not on it. For many people these are the largest line items they own, and any analysis that silently excludes them is not conservative, it is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your question&lt;/th&gt;
&lt;th&gt;Raw Claude&lt;/th&gt;
&lt;th&gt;With a connector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"What does a hawkish Fed do to growth stocks?"&lt;/td&gt;
&lt;td&gt;Good answer. Use it.&lt;/td&gt;
&lt;td&gt;No benefit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Explain how a Monte Carlo projection works"&lt;/td&gt;
&lt;td&gt;Good answer. Use it.&lt;/td&gt;
&lt;td&gt;No benefit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"How concentrated is my portfolio right now?"&lt;/td&gt;
&lt;td&gt;Cannot know&lt;/td&gt;
&lt;td&gt;Computed from your holdings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"What is my realized gain this year?"&lt;/td&gt;
&lt;td&gt;Cannot know&lt;/td&gt;
&lt;td&gt;Computed from your transactions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"If equities drop 15%, what happens to me?"&lt;/td&gt;
&lt;td&gt;Guesses a number&lt;/td&gt;
&lt;td&gt;Point estimate per holding, via beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"When do I hit my FIRE number, and how wide is the range?"&lt;/td&gt;
&lt;td&gt;Estimates, differently each ask&lt;/td&gt;
&lt;td&gt;Monte Carlo over stated assumptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Include my jeonse deposit and physical gold"&lt;/td&gt;
&lt;td&gt;Invisible&lt;/td&gt;
&lt;td&gt;Tracked as real assets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What Opula actually adds
&lt;/h2&gt;

&lt;p&gt;Not intelligence. Claude already has that, and Opula does not try to replace the reasoning with a rule.&lt;/p&gt;

&lt;p&gt;Opula gives Claude two things it cannot get on its own: a durable memory of what you own, in one currency, with cost basis and history, and a set of deterministic tools that compute over it. Concentration, correlation, drawdown, savings rate, net-worth attribution, and forward projections come back as rule-computed data, the same numbers on the second ask as on the first. Claude then does what it is good at, which is reading those numbers with you and arguing about what they mean.&lt;/p&gt;

&lt;p&gt;The division of labor is deliberate: Opula returns data, not narrative, and never tells you what to buy. It is diagnostic data and education, not licensed advice. What to do about it stays between you and Claude, and for anything with a tax or legal edge, your local professional.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can Claude analyze my portfolio without a connector?&lt;/strong&gt; It can analyze holdings you paste into the chat, for that one conversation. It has no memory of them afterward, no cost basis, and no price history, so anything requiring your past (realized gain, drawdown, correlation over time) is out of reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do I get different numbers when I ask Claude the same question twice?&lt;/strong&gt; LLM inference is nondeterministic in practice, even at temperature zero, largely because the server batch your request joins varies. That is fine for prose and not fine for a percentage of your net worth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a connector just a way to feed Claude my data?&lt;/strong&gt; Feeding it data is half. The other half is that the computation happens outside the model, by rule, so the number cannot drift or be estimated into existence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I still need Claude if I use Opula?&lt;/strong&gt; Yes, and that is the design. Opula has no opinions and writes no narrative. It returns facts about your money; Claude is what makes sense of them.&lt;/p&gt;

&lt;p&gt;If you want to try the connector side of this, I am building &lt;a href="https://opula.io?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=can_claude_analyze_portfolio" rel="noopener noreferrer"&gt;Opula&lt;/a&gt;, a hosted MCP server that gives Claude a durable, computed view of your own money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/" rel="noopener noreferrer"&gt;Defeating Nondeterminism in LLM Inference (Thinking Machines Lab, September 2025)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/html/2504.00042v2" rel="noopener noreferrer"&gt;Beyond the Reported Cutoff: where LLMs fall short on financial knowledge (arXiv, 2025)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>mcp</category>
      <category>fintech</category>
    </item>
    <item>
      <title>What building a remote MCP server taught me about authentication</title>
      <dc:creator>Opula</dc:creator>
      <pubDate>Mon, 13 Jul 2026 18:11:05 +0000</pubDate>
      <link>https://dev.to/opula_io/what-building-a-remote-mcp-server-taught-me-about-authentication-59m2</link>
      <guid>https://dev.to/opula_io/what-building-a-remote-mcp-server-taught-me-about-authentication-59m2</guid>
      <description>&lt;p&gt;When I started building &lt;a href="https://opula.io?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_auth" rel="noopener noreferrer"&gt;Opula&lt;/a&gt; - a hosted MCP server that lets Claude read your portfolio, net worth, and cash flow so you can just &lt;em&gt;ask&lt;/em&gt; about your own money - I assumed the hard part would be the finance logic. It wasn't. The hard part was authentication.&lt;/p&gt;

&lt;p&gt;MCP moves fast, the auth story changed twice in 2025, and most of the "add auth to your MCP server" tutorials online are already out of date. So here is the mental model I wish I'd had on day one: how transports shape your auth options, what the current spec actually requires, and the standards (and anti-patterns) that matter in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: auth depends entirely on your transport
&lt;/h2&gt;

&lt;p&gt;MCP defines two standard transports, and they live in completely different security worlds.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;STDIO&lt;/th&gt;
&lt;th&gt;Streamable HTTP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Local subprocess&lt;/td&gt;
&lt;td&gt;Remote network service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clients&lt;/td&gt;
&lt;td&gt;1:1 with the host&lt;/td&gt;
&lt;td&gt;N clients, multi-user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network exposure&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth model&lt;/td&gt;
&lt;td&gt;OS process isolation, env vars&lt;/td&gt;
&lt;td&gt;OAuth 2.1, bearer tokens, API keys, mTLS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-machine&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(SSE was a third, earlier transport. It's deprecated now - if you're starting today, don't.)&lt;/p&gt;

&lt;p&gt;The takeaway: &lt;strong&gt;if your server runs locally over STDIO, you mostly don't need auth.&lt;/strong&gt; The client already trusts the process it spawned, and you pass secrets via environment variables. The official guidance is explicit about this: for STDIO, use environment-based credentials instead of OAuth.&lt;/p&gt;

&lt;p&gt;The moment you go remote over Streamable HTTP - which is what any hosted/SaaS MCP server is - the picture flips. Now you have a single HTTP endpoint (say &lt;code&gt;POST /api/mcp&lt;/code&gt;) reachable over the network, potentially by many users, and you own the entire authentication and authorization story.&lt;/p&gt;

&lt;p&gt;Opula is remote by nature (your Claude talks to &lt;code&gt;https://opula.io/api/mcp&lt;/code&gt;), so everything below is about the HTTP side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spec you should actually read: 2025-06-18
&lt;/h2&gt;

&lt;p&gt;The MCP authorization spec was revised in June 2025, and this revision is the one to build against. The single most important sentence in it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A protected MCP server acts as an &lt;strong&gt;OAuth 2.1 resource server&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one line reframes the whole problem. Your MCP server is &lt;em&gt;not&lt;/em&gt; an identity provider. It doesn't own login screens or issue tokens. It &lt;strong&gt;validates&lt;/strong&gt; tokens that some authorization server issued, and it serves protected resources (your tools) when the token checks out.&lt;/p&gt;

&lt;p&gt;The June revision made two changes worth knowing if you read older tutorials:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It &lt;strong&gt;cleanly separated the resource server (your MCP server) from the authorization server.&lt;/strong&gt; You're encouraged to delegate token issuance to a dedicated auth server rather than bolt an OAuth provider onto your tool server.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;removed the old fallback default endpoints&lt;/strong&gt; (&lt;code&gt;/authorize&lt;/code&gt;, &lt;code&gt;/token&lt;/code&gt;, &lt;code&gt;/register&lt;/code&gt;) in favor of mandatory metadata discovery via RFC 9728.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The standards stack
&lt;/h2&gt;

&lt;p&gt;Modern MCP auth is basically four OAuth RFCs wearing a trench coat. Here's the whole cast:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RFC&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFC 9728&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Resource Server&lt;/td&gt;
&lt;td&gt;Protected Resource Metadata - your server advertises &lt;em&gt;which&lt;/em&gt; auth servers to trust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFC 8414&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authorization Server&lt;/td&gt;
&lt;td&gt;AS Metadata - the auth server advertises its endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFC 7591&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;Dynamic Client Registration - clients register themselves without manual setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFC 8707&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Token audience&lt;/td&gt;
&lt;td&gt;Resource Indicators - tokens are bound to a specific target resource&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus &lt;strong&gt;OAuth 2.1 Authorization Code + PKCE&lt;/strong&gt; as the grant flow. Let's walk the flow the way a client actually experiences it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discovery flow, step by step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Client hits your endpoint with no token.&lt;/strong&gt; You reject it with a &lt;code&gt;401&lt;/code&gt; - but not a bare 401. You include a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header pointing at your metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt; &lt;span class="ne"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer realm="mcp",&lt;/span&gt;
&lt;span class="s"&gt;  resource_metadata="https://opula.io/.well-known/oauth-protected-resource"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Client fetches your Protected Resource Metadata (RFC 9728).&lt;/strong&gt; This is a tiny JSON document at a well-known URL that says "here's who I am and who issues tokens for me":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://opula.io/api/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://auth.opula.io"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bearer_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp:tools:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp:portfolio:read"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the piece that older guides skip and the June spec makes mandatory. The client no longer &lt;em&gt;guesses&lt;/em&gt; your auth endpoints - you tell it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Client discovers the authorization server (RFC 8414)&lt;/strong&gt;, optionally registers itself (RFC 7591), and runs a standard &lt;strong&gt;OAuth 2.1 Authorization Code flow with PKCE&lt;/strong&gt;. PKCE is not optional here: MCP clients like Claude Desktop and Cursor are public clients that can't safely store a client secret, so the spec &lt;em&gt;mandates&lt;/em&gt; PKCE for everyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Client requests a token bound to your resource (RFC 8707).&lt;/strong&gt; It sends a &lt;code&gt;resource&lt;/code&gt; parameter so the issued token's audience (&lt;code&gt;aud&lt;/code&gt;) is specifically &lt;em&gt;your&lt;/em&gt; server, not "any API this user can reach."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Client retries with the token:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/api/mcp&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;opula.io&lt;/span&gt;
&lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer eyJhbGciOiJSUzI1NiIs...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you validate and serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validating the token on the server
&lt;/h2&gt;

&lt;p&gt;Whatever framework you use, token validation on a resource server comes down to the same checks. Here's the shape of it in a Node handler (Opula runs on Node on Vercel):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StreamableHTTPServerTransport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/sdk/server/streamableHttp.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Claims&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyJwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;jwks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AUTH_SERVER_JWKS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// The checks that actually matter:&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;iss&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;TRUSTED_ISSUER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// who issued it&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aud&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://opula.io/api/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// issued FOR us (RFC 8707)&lt;/span&gt;
  &lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;nowSeconds&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;                 &lt;span class="c1"&gt;// not expired&lt;/span&gt;
  &lt;span class="nf"&gt;assertScopes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;requiredScopes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// allowed to do this&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The audience check (&lt;code&gt;aud&lt;/code&gt;) is the one people drop, and it's the one that stops the scariest attacks. Which brings me to the mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three anti-patterns I had to design around
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Token passthrough - explicitly forbidden.&lt;/strong&gt; The tempting shortcut is: let the client hand you a token for some downstream API (say a brokerage PAT) and just forward it. The spec says &lt;strong&gt;MUST NOT&lt;/strong&gt;. If you accept tokens that weren't issued &lt;em&gt;for you&lt;/em&gt;, you break audit trails, blur identities in downstream logs, and turn your server into a proxy for stolen tokens.&lt;/p&gt;

&lt;p&gt;The fix is a design rule: &lt;strong&gt;the MCP server is the system of record.&lt;/strong&gt; Clients authenticate &lt;em&gt;to&lt;/em&gt; you with MCP-scoped credentials; you then use your &lt;em&gt;own&lt;/em&gt; managed credentials to call downstream services. Opula calls its market-data sources (like the FRED economic data API) with server-side keys that never touch the client - which, conveniently, is exactly what "no token passthrough" demands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The confused deputy.&lt;/strong&gt; If your server uses a single static client ID to talk to a third-party provider on behalf of every user, that provider may skip the consent screen for a request that &lt;em&gt;looks&lt;/em&gt; trusted. An attacker can ride that to get a user authorized to something they never intended. Mitigation: dynamic client registration and explicit user consent, strict redirect-URI validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Forgetting you're on a network.&lt;/strong&gt; Two cheap but critical HTTP-transport rules from the spec: &lt;strong&gt;validate the &lt;code&gt;Origin&lt;/code&gt; header&lt;/strong&gt; on every connection (prevents DNS-rebinding attacks), require HTTPS in production, and if you ever run locally, bind to &lt;code&gt;127.0.0.1&lt;/code&gt; rather than &lt;code&gt;0.0.0.0&lt;/code&gt;. One more from hard-won ops experience: &lt;strong&gt;scrub the &lt;code&gt;Authorization&lt;/code&gt; header out of your logs.&lt;/strong&gt; It's a bearer token; logging it is the same as logging a password.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pragmatic path: start with bearer, grow into OAuth
&lt;/h2&gt;

&lt;p&gt;Here's the honest part. Full RFC 9728 + 8414 + 7591 + 8707 + PKCE is the destination, not necessarily your day-one commit. Plenty of shipping MCP servers - Opula included - start with a validated &lt;strong&gt;bearer token over Streamable HTTP&lt;/strong&gt;: the client sends &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt;, middleware verifies signature, issuer, audience, expiry, and scope &lt;em&gt;before&lt;/em&gt; the request ever reaches the MCP transport layer, and everything not explicitly authorized gets a clean &lt;code&gt;401&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That gets you a secure, multi-user server today. The value of understanding the full spec is that it tells you exactly where the seams are when you grow: the day a client wants to self-register, or an enterprise wants its own IdP to issue the tokens, you already know which RFC slots in where instead of re-architecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pick your transport first.&lt;/strong&gt; STDIO = local, use env vars, skip OAuth. Streamable HTTP = remote, you own auth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your MCP server is an OAuth 2.1 resource server&lt;/strong&gt;, not an identity provider. It validates tokens; it doesn't mint them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery is mandatory now:&lt;/strong&gt; &lt;code&gt;401&lt;/code&gt; + &lt;code&gt;WWW-Authenticate&lt;/code&gt; -&amp;gt; Protected Resource Metadata (RFC 9728) -&amp;gt; auth server -&amp;gt; Authorization Code + &lt;strong&gt;PKCE&lt;/strong&gt; -&amp;gt; audience-bound token (RFC 8707).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always check &lt;code&gt;aud&lt;/code&gt;.&lt;/strong&gt; A token not issued &lt;em&gt;for your server&lt;/em&gt; is a token you must reject.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never do token passthrough.&lt;/strong&gt; Be the system of record; use your own downstream credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can ship a validated bearer token today&lt;/strong&gt; and grow into the full flow - as long as you know where the seams are.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Further reading
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;MCP Authorization spec (2025-06-18): &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;MCP Security Best Practices: &lt;a href="https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 9728 - OAuth 2.0 Protected Resource Metadata&lt;/li&gt;
&lt;li&gt;RFC 8707 - Resource Indicators for OAuth 2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;I'm building &lt;a href="https://opula.io?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=mcp_auth" rel="noopener noreferrer"&gt;Opula&lt;/a&gt;, a hosted MCP server that turns Claude into an analyst for your own money. If you're building MCP servers too, I'd love to compare notes on where you drew the auth line.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>security</category>
      <category>oauth</category>
    </item>
  </channel>
</rss>
