<?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: Wiktor Małyska</title>
    <description>The latest articles on DEV Community by Wiktor Małyska (@wiktormalyska).</description>
    <link>https://dev.to/wiktormalyska</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%2F3838408%2Fe021ffb0-c28c-4737-a5ce-a769ecae79be.jpeg</url>
      <title>DEV Community: Wiktor Małyska</title>
      <link>https://dev.to/wiktormalyska</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wiktormalyska"/>
    <language>en</language>
    <item>
      <title>node --test only learned about globs in Node 21</title>
      <dc:creator>Wiktor Małyska</dc:creator>
      <pubDate>Tue, 01 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/wiktormalyska/node-test-only-learned-about-globs-in-node-21-252k</link>
      <guid>https://dev.to/wiktormalyska/node-test-only-learned-about-globs-in-node-21-252k</guid>
      <description>&lt;p&gt;My package declares support for Node 18 and up. Its own test suite could not run on two of the three versions I claimed to support. Everything passed on my machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&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;"scripts"&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;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node --test &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;test/**/*.test.mjs&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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;span class="nl"&gt;"engines"&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;span class="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=18"&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;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;That looks fine. It is quoted, so the shell does not expand it, which is what you want when a tool does its own glob handling. On my Node 22 it ran 35 tests and went green.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CI said
&lt;/h2&gt;

&lt;p&gt;The workflow was nine jobs: Ubuntu, Windows and macOS across Node 18, 20 and 22.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Could not find '/home/runner/work/pkg/pkg/test/**/*.test.mjs'
&lt;/span&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node &lt;span class="nt"&gt;--test&lt;/span&gt; &lt;span class="s2"&gt;"test/**/*.test.mjs"&lt;/span&gt;
&lt;span class="go"&gt;Error: Process completed with exit code 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS 22 green. Ubuntu 18 red. Windows 20 red. The pattern was not the operating system, it was the Node version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Glob expansion in the Node test runner arrived in &lt;strong&gt;Node 21&lt;/strong&gt;. Before that, a path argument is treated as a literal path. So on 18 and 20 the runner looks for a directory literally named &lt;code&gt;**&lt;/code&gt;, does not find it, and dies before a single test executes.&lt;/p&gt;

&lt;p&gt;It never even reached the tests. The suite was not failing; it was not running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&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;"scripts"&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;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node --test"&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;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;With no path argument, the runner discovers test files itself using its default patterns, which include &lt;code&gt;**/*.test.mjs&lt;/code&gt; and anything under a &lt;code&gt;test/&lt;/code&gt; directory. That behaviour has been there since Node 18, and it does not depend on the shell, so it works the same on Windows.&lt;/p&gt;

&lt;p&gt;Same 35 tests, now on all nine jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part worth keeping
&lt;/h2&gt;

&lt;p&gt;This is the entire argument for a build matrix, and it is not thoroughness for its own sake.&lt;/p&gt;

&lt;p&gt;"Works on my machine" is a true statement about exactly one row of that matrix. I had declared support for a range I had never once executed. The matrix did not find a rare edge case; it found that two thirds of my stated support was fiction, about ninety seconds after I pushed the workflow.&lt;/p&gt;

&lt;p&gt;If your package declares an &lt;code&gt;engines&lt;/code&gt; range, run your tests across that range. Otherwise the range is a guess you are asking your users to verify for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/wiktormalyska/mcp-secrets-runner" rel="noopener noreferrer"&gt;https://github.com/wiktormalyska/mcp-secrets-runner&lt;/a&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>testing</category>
      <category>javascript</category>
      <category>devops</category>
    </item>
    <item>
      <title>1Password's secret masking silently corrupts MCP servers</title>
      <dc:creator>Wiktor Małyska</dc:creator>
      <pubDate>Sun, 30 Aug 2026 13:00:01 +0000</pubDate>
      <link>https://dev.to/wiktormalyska/1passwords-secret-masking-silently-corrupts-mcp-servers-312b</link>
      <guid>https://dev.to/wiktormalyska/1passwords-secret-masking-silently-corrupts-mcp-servers-312b</guid>
      <description>&lt;p&gt;I added 1Password support to an MCP secrets runner and the first thing I had to do was turn one of its security features off. Here is why that was the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What op run does
&lt;/h2&gt;

&lt;p&gt;1Password's CLI resolves &lt;code&gt;op://&lt;/code&gt; references and hands the values to a child process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;op run &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./mcp.env &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @stripe/mcp@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also does something thoughtful on top. It watches the child's output and rewrites anything matching a resolved secret to &lt;code&gt;&amp;lt;concealed by 1Password&amp;gt;&lt;/code&gt;. If your program accidentally logs a token, or you are screen sharing, the secret does not leave the machine.&lt;/p&gt;

&lt;p&gt;For a normal CLI this is a good default. I would keep it on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an MCP server is
&lt;/h2&gt;

&lt;p&gt;An MCP server on the stdio transport speaks JSON-RPC over stdout. Not log lines. A framed protocol stream, where the client parses every byte:&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="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;&lt;span class="err"&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;stdout is not a place for human-readable output. It is a wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  The collision
&lt;/h2&gt;

&lt;p&gt;Put those two together. Masking rewrites bytes in a stream that a parser is reading with exact expectations. If a secret value ever appears inside a frame, the frame changes length, and the parse fails.&lt;/p&gt;

&lt;p&gt;What makes this genuinely nasty is &lt;em&gt;where&lt;/em&gt; it fails. The MCP client reports a protocol error. Nothing in that error mentions 1Password, masking, or secrets. You would go and read your server's serialisation code, which is fine, for a long time.&lt;/p&gt;

&lt;p&gt;So the provider passes &lt;code&gt;--no-masking&lt;/code&gt; by default, and the README says why in as many words. There is an opt-in flag to turn masking back on for servers that do not use stdio.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;What I find worth writing down is that &lt;strong&gt;nothing here is a bug&lt;/strong&gt;. Not in 1Password, not in MCP, not in my code.&lt;/p&gt;

&lt;p&gt;A security feature assumes it may rewrite a child's output. A wire protocol assumes it owns stdout exclusively. Both assumptions are reasonable in isolation. The failure lives in the space between them, and it is silent, because neither component is in a position to notice the other exists.&lt;/p&gt;

&lt;p&gt;Every integration you write has a few of these. They do not show up in either project's issue tracker, because from each side nothing is wrong. They show up in yours.&lt;/p&gt;

&lt;p&gt;The practical takeaway: when you wrap a process that speaks a protocol on stdout, audit everything in the chain for the assumption that stdout is for humans. Log prefixes, colour codes, progress bars, and yes, secret masking.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/mcp-secrets-runner" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/mcp-secrets-runner&lt;/a&gt;&lt;/p&gt;

</description>
      <category>1password</category>
      <category>security</category>
      <category>debugging</category>
      <category>node</category>
    </item>
    <item>
      <title>Your .mcp.json probably has a live API key in it</title>
      <dc:creator>Wiktor Małyska</dc:creator>
      <pubDate>Fri, 28 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/wiktormalyska/your-mcpjson-probably-has-a-live-api-key-in-it-4ge5</link>
      <guid>https://dev.to/wiktormalyska/your-mcpjson-probably-has-a-live-api-key-in-it-4ge5</guid>
      <description>&lt;p&gt;Nearly every guide to setting up an MCP server tells you to do this:&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;"mcpServers"&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;span class="nl"&gt;"stripe"&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;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@stripe/mcp@latest"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;span class="nl"&gt;"STRIPE_SECRET_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sk_live_51J..."&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;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;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;That is a live production key sitting in a file that gets committed to repositories, synced between machines, screenshotted for tutorials and pasted into bug reports. I have seen &lt;code&gt;sk_live_&lt;/code&gt; keys in all four.&lt;/p&gt;

&lt;p&gt;It is also, in my experience, the single biggest reason MCP stalls at a company's security review. Not the protocol. Not the tooling. The config file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious fix has its own failure mode
&lt;/h2&gt;

&lt;p&gt;So you wrap the server in your secret manager's CLI instead:&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="nl"&gt;"args"&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;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--env=prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@stripe/mcp@latest"&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;Better. The key is gone from the file. But those CLIs authenticate with a developer session stored in your OS keyring, and that session expires.&lt;/p&gt;

&lt;p&gt;When it does, &lt;strong&gt;every&lt;/strong&gt; MCP server wrapped this way stops starting at the same moment. And the error surfaces in your MCP client, which knows nothing about secret managers or expired logins. You get a generic startup failure on ten servers at once and go looking in entirely the wrong place.&lt;/p&gt;

&lt;p&gt;I lost an afternoon to this before the pattern clicked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Machine identities do not expire
&lt;/h2&gt;

&lt;p&gt;Every serious secret manager has a concept for unattended access: a credential meant for machines rather than humans, with no TTL of its own. Infisical calls it a machine identity, 1Password calls it a service account, Vault calls it AppRole.&lt;/p&gt;

&lt;p&gt;The credential is long-lived; what it produces is short-lived. You exchange it for a token at process start, use the token, and never think about it again.&lt;/p&gt;

&lt;p&gt;That is the whole idea behind &lt;code&gt;mcp-secrets-runner&lt;/code&gt;. It sits between the MCP client and the server, authenticates with a machine credential, fetches the secrets and execs the real server:&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;"mcpServers"&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;span class="nl"&gt;"stripe"&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;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&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-secrets-runner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--env=prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--path=/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@stripe/mcp@latest"&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;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;No secret values in the file. No session to renew. The same config works on every machine that has the machine credentials in its environment, which is what makes it safe to commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three backends, three different shapes
&lt;/h2&gt;

&lt;p&gt;The interesting part of building this was that the three supported backends work nothing alike:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Infisical&lt;/strong&gt; mints a token over HTTPS, then hands off to the Infisical CLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1Password&lt;/strong&gt; hands off to &lt;code&gt;op run&lt;/code&gt;, which authenticates itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vault&lt;/strong&gt; reads the secret over the HTTP API and starts your MCP server directly, with no CLI involved at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A provider only has to answer one question: which child process do I start, and what environment does it get. That the three answers look so different is the best evidence I have that the interface is not overfitted to the first one I wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it does not work
&lt;/h2&gt;

&lt;p&gt;"My MCP server won't start" is almost always one of four things, and an MCP client reports all four identically. So there is a &lt;code&gt;doctor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mcp-secrets-runner doctor &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vault
&lt;span class="go"&gt;
mcp-secrets-runner doctor (provider: vault)

  [ok  ] Node 22.14.0
  [ok  ] instance
         http://127.0.0.1:8200
  [ok  ] AppRole credentials present
  [ok  ] cached token available
  [ok  ] authentication succeeded
         token valid for 20 min

All checks passed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Missing CLI, missing credentials, wrong instance URL, unreadable secret path. One line each, instead of one opaque failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two details worth stealing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;All diagnostics go to stderr.&lt;/strong&gt; stdout belongs to the MCP protocol. One stray byte there corrupts the JSON-RPC stream and the client reports a parse error pointing nowhere useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It fails closed.&lt;/strong&gt; If credentials are set but authentication fails, the runner exits. The alternative is worse than it sounds: a credential-less CLI invocation opens an interactive browser login, which hangs the MCP server on stdin forever while the client waits.&lt;/p&gt;

&lt;p&gt;MIT, zero dependencies, Node 18+.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/wiktormalyska/mcp-secrets-runner" rel="noopener noreferrer"&gt;https://github.com/wiktormalyska/mcp-secrets-runner&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>security</category>
      <category>devops</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop Hardcoding Your Portfolio Projects. I Built an API to Turn GitHub into a Headless CMS</title>
      <dc:creator>Wiktor Małyska</dc:creator>
      <pubDate>Sun, 22 Mar 2026 13:27:32 +0000</pubDate>
      <link>https://dev.to/wiktormalyska/stop-hardcoding-your-portfolio-projects-i-built-an-api-to-turn-github-into-a-headless-cms-4n3k</link>
      <guid>https://dev.to/wiktormalyska/stop-hardcoding-your-portfolio-projects-i-built-an-api-to-turn-github-into-a-headless-cms-4n3k</guid>
      <description>&lt;p&gt;Every time I finished a new side project, I faced the exact same annoying routine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open my portfolio repository.&lt;/li&gt;
&lt;li&gt;Find the &lt;code&gt;projects.json&lt;/code&gt; or hardcoded array.&lt;/li&gt;
&lt;li&gt;Manually type in the new project name, description, and tech stack.&lt;/li&gt;
&lt;li&gt;Download a screenshot, put it in the &lt;code&gt;public&lt;/code&gt; folder, and link it.&lt;/li&gt;
&lt;li&gt;Push changes and wait for the deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As developers, we automate everything, yet we still update our portfolios like it's 2010. I got tired of this, so I built a solution: &lt;strong&gt;PortfolioAPI&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is PortfolioAPI?
&lt;/h2&gt;

&lt;p&gt;It’s a simple tool that turns your GitHub repositories into a Headless CMS for your portfolio. Instead of manually updating your frontend code, your portfolio fetches data directly from your GitHub repos via one fast API call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Link:&lt;/strong&gt; &lt;a href="https://portfolioapi.wiktormalyska.ovh/" rel="noopener noreferrer"&gt;PortfolioAPI&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  How it works (It takes 2 minutes)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Drop a &lt;code&gt;meta.json&lt;/code&gt; in your repo&lt;/strong&gt;&lt;br&gt;
You just create a &lt;code&gt;meta.json&lt;/code&gt; file in the root of any public GitHub repository you want to showcase.&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My Awesome Project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A short description shown on the portfolio page."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imageUrl"&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://raw.githubusercontent.com/your-username/your-repo/main/assets/screenshot.png](https://raw.githubusercontent.com/your-username/your-repo/main/assets/screenshot.png)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"websiteUrl"&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://my-project.example.com](https://my-project.example.com)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"technologies"&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;span class="s2"&gt;"React"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"TypeScript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"TailwindCSS"&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;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;&lt;strong&gt;Step 2: Fetch the data in your frontend&lt;/strong&gt;&lt;br&gt;
Make a single GET request using your API key. The API scans your GitHub, finds all repos with a &lt;code&gt;meta.json&lt;/code&gt;, and returns a clean, structured array of your projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchProjects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&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;response&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[https://api.portfolioapi.wiktormalyska.ovh/projects](https://api.portfolioapi.wiktormalyska.ovh/projects)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-API-KEY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;your_api_key_here&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Boom! Ready to map over in React/Vue/Svelte&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why did I build it this way?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Database Needed:&lt;/strong&gt; Your code lives on GitHub anyway, why not keep its metadata there too?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always Up to Date:&lt;/strong&gt; Update the &lt;code&gt;meta.json&lt;/code&gt; in your project repo, and your portfolio updates instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blazing Fast:&lt;/strong&gt; I implemented server-side caching so the GitHub data is delivered in milliseconds. No unnecessary noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tech Agnostic:&lt;/strong&gt; It doesn't matter if your portfolio is built in React, Vue, Next.js, or vanilla HTML. It’s just a standard REST API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Right now, the API handles reading GitHub portfolios perfectly. I'm currently working on adding a dedicated Node.js package and an email contact form endpoint to make it an all-in-one backend for developer portfolios.&lt;/p&gt;

&lt;p&gt;I’m an indie developer trying to build tools that actually save time. I’d love to hear your feedback. What do you think about managing portfolio content directly from GitHub repos? &lt;/p&gt;

&lt;p&gt;Let me know in the comments! 👇&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Psst... with code FREETRIAL you can get first month for free! (for first 10 customers)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>github</category>
    </item>
  </channel>
</rss>
