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">"MY_API_KEY is required"</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">"mcpServers"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"my-tool"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"command"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"npx"</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"args"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">"-y"</span><span class="hljs-punctuation">,</span> <span class="hljs-string">"my-tool"</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
<span class="hljs-attr">"env"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
<span class="hljs-attr">"MY_API_KEY"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"${MY_API_KEY}"</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">"fs/promises"</span>;
<span class="hljs-keyword">import</span> { homedir } <span class="hljs-keyword">from</span> <span class="hljs-string">"os"</span>;
<span class="hljs-keyword">import</span> { join } <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</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">".my-tool"</span>, <span class="hljs-string">"config.json"</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">"utf-8"</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">"Config not found. Run: my-tool setup"</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">"https://auth.service.com/device/code"</span>,
{ <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</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> =></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 < <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">"https://auth.service.com/oauth/token"</span>, {
<span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</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">"urn:ietf:params:oauth:grant-type:device_code"</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">"Authentication timed out"</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">"fs/promises"</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">".my-tool"</span>, <span class="hljs-string">"token.json"</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">"utf-8"</span>));
<span class="hljs-keyword">if</span> (cached.<span class="hljs-property">expires_at</span> > <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">
<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">
<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">"Authentication required. Set MY_TOOL_TOKEN env var or run setup."</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">
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Error</span>(<span class="hljs-string">"401: invalid_grant"</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">"text"</span>,
<span class="hljs-attr">text</span>: <span class="hljs-string">"❌ Authentication failed. Your API key may be expired. "</span> +
<span class="hljs-string">"Get a new key at https://service.com/keys and update your MCP config."</span>
}]
};
Choose the right pattern for your tool's security needs. Start simple, add complexity only when needed.
#MCP #Tutorial #Authentication #Security #mcpm
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">"MY_API_KEY is required"</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">"mcpServers"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"my-tool"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"command"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"npx"</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">"args"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">"-y"</span><span class="hljs-punctuation">,</span> <span class="hljs-string">"my-tool"</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">"env"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"MY_API_KEY"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"${MY_API_KEY}"</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">"fs/promises"</span>; <span class="hljs-keyword">import</span> { homedir } <span class="hljs-keyword">from</span> <span class="hljs-string">"os"</span>; <span class="hljs-keyword">import</span> { join } <span class="hljs-keyword">from</span> <span class="hljs-string">"path"</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">".my-tool"</span>, <span class="hljs-string">"config.json"</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">"utf-8"</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">"Config not found. Run: my-tool setup"</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">"https://auth.service.com/device/code"</span>, { <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</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> =></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 < <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">"https://auth.service.com/oauth/token"</span>, { <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</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">"urn:ietf:params:oauth:grant-type:device_code"</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">"Authentication timed out"</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">"fs/promises"</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">".my-tool"</span>, <span class="hljs-string">"token.json"</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">"utf-8"</span>)); <span class="hljs-keyword">if</span> (cached.<span class="hljs-property">expires_at</span> > <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">"Authentication required. Set MY_TOOL_TOKEN env var or run setup."</span> ); process.<span class="hljs-title function_">exit</span>(<span class="hljs-number">1</span>); }Security Checklist
process.exit(1)when auth fails — don't start the serverError 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">"401: invalid_grant"</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">"text"</span>, <span class="hljs-attr">text</span>: <span class="hljs-string">"❌ Authentication failed. Your API key may be expired. "</span> + <span class="hljs-string">"Get a new key at https://service.com/keys and update your MCP config."</span> }] };Choose the right pattern for your tool's security needs. Start simple, add complexity only when needed.
#MCP #Tutorial #Authentication #Security #mcpm