← Back to Blog
tutorialauthenticationsecuritypatterns

Authentication Patterns for MCP Tools

Secure your MCP tools with API keys, OAuth, environment variables, and token management strategies.

·3 min read·xapable

Authentication Patterns for MCP Tools

Most MCP tools need authentication — API keys, tokens, or OAuth. This guide covers secure patterns for every scenario.

Pattern 1: Environment Variables (Most Common)

Best for API keys and static tokens.

<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">API_KEY</span> = process.<span class="hljs-property">env</span>.<span class="hljs-property">MY_API_KEY</span>;
<span class="hljs-keyword">if</span> (!<span class="hljs-variable constant_">API_KEY</span>) {
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">&quot;MY_API_KEY is required&quot;</span>);
  process.<span class="hljs-title function_">exit</span>(<span class="hljs-number">1</span>);
}

User configures in their MCP client:

<span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;mcpServers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;my-tool&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;command&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;npx&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;args&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;-y&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-string">&quot;my-tool&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;env&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;MY_API_KEY&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;${MY_API_KEY}&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>

Pros: Simple, secure, works everywhere
Cons: Tokens are static; rotation requires config update

Pattern 2: Config File

For tools with complex configuration.

<span class="hljs-keyword">import</span> { readFile } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;fs/promises&quot;</span>;
<span class="hljs-keyword">import</span> { homedir } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;os&quot;</span>;
<span class="hljs-keyword">import</span> { join } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;path&quot;</span>;

<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">loadConfig</span>(<span class="hljs-params"></span>) {
  <span class="hljs-keyword">const</span> path = <span class="hljs-title function_">join</span>(<span class="hljs-title function_">homedir</span>(), <span class="hljs-string">&quot;.my-tool&quot;</span>, <span class="hljs-string">&quot;config.json&quot;</span>);
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(<span class="hljs-keyword">await</span> <span class="hljs-title function_">readFile</span>(path, <span class="hljs-string">&quot;utf-8&quot;</span>));
  } <span class="hljs-keyword">catch</span> {
    <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">&quot;Config not found. Run: my-tool setup&quot;</span>);
    process.<span class="hljs-title function_">exit</span>(<span class="hljs-number">1</span>);
  }
}

<span class="hljs-keyword">const</span> config = <span class="hljs-keyword">await</span> <span class="hljs-title function_">loadConfig</span>();

Pros: Supports multiple credentials, complex settings
Cons: Extra setup step; file permissions matter

Pattern 3: OAuth Device Flow

For tools that need user-specific access.

<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">authenticate</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">// Step 1: Request device code</span>
  <span class="hljs-keyword">const</span> { device_code, user_code, verification_uri } = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(
    <span class="hljs-string">&quot;https://auth.service.com/device/code&quot;</span>,
    { <span class="hljs-attr">method</span>: <span class="hljs-string">&quot;POST&quot;</span>, <span class="hljs-attr">body</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>({ <span class="hljs-attr">client_id</span>: <span class="hljs-variable constant_">CLIENT_ID</span> }) }
  ).<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">r</span> =&gt;</span> r.<span class="hljs-title function_">json</span>());

  <span class="hljs-comment">// Step 2: Tell user to authenticate</span>
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">`Please visit: <span class="hljs-subst">${verification_uri}</span>`</span>);
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(<span class="hljs-string">`Enter code: <span class="hljs-subst">${user_code}</span>`</span>);

  <span class="hljs-comment">// Step 3: Poll for token</span>
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">30</span>; i++) {
    <span class="hljs-keyword">await</span> <span class="hljs-title function_">sleep</span>(<span class="hljs-number">2000</span>);
    <span class="hljs-keyword">const</span> resp = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-string">&quot;https://auth.service.com/oauth/token&quot;</span>, {
      <span class="hljs-attr">method</span>: <span class="hljs-string">&quot;POST&quot;</span>,
      <span class="hljs-attr">body</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>({
        <span class="hljs-attr">client_id</span>: <span class="hljs-variable constant_">CLIENT_ID</span>,
        device_code,
        <span class="hljs-attr">grant_type</span>: <span class="hljs-string">&quot;urn:ietf:params:oauth:grant-type:device_code&quot;</span>
      })
    });

    <span class="hljs-keyword">if</span> (resp.<span class="hljs-property">ok</span>) {
      <span class="hljs-keyword">const</span> { access_token, refresh_token } = <span class="hljs-keyword">await</span> resp.<span class="hljs-title function_">json</span>();
      <span class="hljs-keyword">return</span> { access_token, refresh_token };
    }
  }

  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">&quot;Authentication timed out&quot;</span>);
}

Pros: User-specific access, refreshable tokens
Cons: Complex implementation; requires browser interaction

Pattern 4: Token Caching

Avoid re-authenticating on every tool call:

<span class="hljs-keyword">import</span> { writeFile, readFile } <span class="hljs-keyword">from</span> <span class="hljs-string">&quot;fs/promises&quot;</span>;

<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">CACHE_PATH</span> = <span class="hljs-title function_">join</span>(<span class="hljs-title function_">homedir</span>(), <span class="hljs-string">&quot;.my-tool&quot;</span>, <span class="hljs-string">&quot;token.json&quot;</span>);

<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">getToken</span>(<span class="hljs-params"></span>) {
  <span class="hljs-comment">// Try cache first</span>
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cached = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(<span class="hljs-keyword">await</span> <span class="hljs-title function_">readFile</span>(<span class="hljs-variable constant_">CACHE_PATH</span>, <span class="hljs-string">&quot;utf-8&quot;</span>));
    <span class="hljs-keyword">if</span> (cached.<span class="hljs-property">expires_at</span> &gt; <span class="hljs-title class_">Date</span>.<span class="hljs-title function_">now</span>()) {
      <span class="hljs-keyword">return</span> cached.<span class="hljs-property">access_token</span>;
    }
  } <span class="hljs-keyword">catch</span> {}

  <span class="hljs-comment">// Get fresh token</span>
  <span class="hljs-keyword">const</span> token = <span class="hljs-keyword">await</span> <span class="hljs-title function_">authenticate</span>();
  <span class="hljs-keyword">await</span> <span class="hljs-title function_">writeFile</span>(<span class="hljs-variable constant_">CACHE_PATH</span>, <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>({
    <span class="hljs-attr">access_token</span>: token.<span class="hljs-property">access_token</span>,
    <span class="hljs-attr">expires_at</span>: <span class="hljs-title class_">Date</span>.<span class="hljs-title function_">now</span>() + <span class="hljs-number">3600</span> * <span class="hljs-number">1000</span> <span class="hljs-comment">// 1 hour</span>
  }));

  <span class="hljs-keyword">return</span> token.<span class="hljs-property">access_token</span>;
}

Pattern 5: CI/CD Authentication

For tools used in automated pipelines:

<span class="hljs-comment">// Accept token via env var OR config file</span>
<span class="hljs-keyword">const</span> <span class="hljs-variable constant_">TOKEN</span> = process.<span class="hljs-property">env</span>.<span class="hljs-property">MY_TOOL_TOKEN</span>
  || (<span class="hljs-keyword">await</span> <span class="hljs-title function_">loadConfig</span>()).<span class="hljs-property">token</span>;

<span class="hljs-keyword">if</span> (!<span class="hljs-variable constant_">TOKEN</span>) {
  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">error</span>(
    <span class="hljs-string">&quot;Authentication required. Set MY_TOOL_TOKEN env var or run setup.&quot;</span>
  );
  process.<span class="hljs-title function_">exit</span>(<span class="hljs-number">1</span>);
}

Security Checklist

  • ✅ Never log tokens or API keys
  • ✅ Validate tokens on startup
  • ✅ Handle expired tokens with clear error messages
  • ✅ Use process.exit(1) when auth fails — don't start the server
  • ✅ Store cached tokens with restricted file permissions
  • ✅ Support token refresh when possible

Error Messages for Auth Failures

<span class="hljs-comment">// ❌ Bad — exposes internals</span>
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">&quot;401: invalid_grant&quot;</span>);

<span class="hljs-comment">// ✅ Good — actionable</span>
<span class="hljs-keyword">return</span> {
  <span class="hljs-attr">content</span>: [{
    <span class="hljs-attr">type</span>: <span class="hljs-string">&quot;text&quot;</span>,
    <span class="hljs-attr">text</span>: <span class="hljs-string">&quot;❌ Authentication failed. Your API key may be expired. &quot;</span> +
          <span class="hljs-string">&quot;Get a new key at https://service.com/keys and update your MCP config.&quot;</span>
  }]
};

Choose the right pattern for your tool's security needs. Start simple, add complexity only when needed.

#MCP #Tutorial #Authentication #Security #mcpm