-
Notifications
You must be signed in to change notification settings - Fork 0
Managing Secrets
Secrets allow action scripts to authenticate with APIs without exposing credentials. Every secret is scoped to specific URL patterns, so it can only be used with the intended services.
Declare secrets in your manifest's secrets section:
secrets:
- name: GITHUB_TOKEN
description: "GitHub personal access token for API access"
scope: "https://api.github.com/*"
required: false
- name: API_KEY
description: "Backend API key"
scope:
- "https://api.myservice.com/*"
- "https://staging.myservice.com/*"
required: true| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Secret identifier (used in ctx.getSecret) |
description |
string | No | Explains what this secret is and where to get it |
scope |
string or string[] | Yes | URL pattern(s) where the secret can be used |
required |
boolean | No | Default: false. If true, server won't start without it |
Scope patterns control which URLs a secret can be sent to. This prevents accidental credential leakage.
A scope pattern is a URL prefix with an optional trailing wildcard (*).
# Exact URL match
scope: "https://api.example.com/v1/auth"
# All paths under a domain
scope: "https://api.example.com/*"
# Specific path prefix
scope: "https://api.example.com/v2/admin/*"- The wildcard
*matches at path boundaries —/*won't match/repos-private -
Exact origin required —
api.example.comwon't matchevil.api.example.com -
No subdomain matching — scope for
example.comwon't matchsub.example.com
# Given scope: "https://api.github.com/*"
# These MATCH:
# https://api.github.com/repos/owner/repo
# https://api.github.com/search/code?q=test
# https://api.github.com/
# These DO NOT MATCH:
# https://evil-api.github.com/repos (different subdomain)
# http://api.github.com/repos (wrong scheme)
# https://api.github.com.evil.com/repos (different domain)Use an array when a secret needs to work with multiple services:
secrets:
- name: GITHUB_TOKEN
description: "GitHub token for API and raw content access"
scope:
- "https://api.github.com/*"
- "https://raw.githubusercontent.com/*"
required: false# GOOD: Specific scope for the service you're using
scope: "https://api.github.com/*"
# GOOD: Multiple specific scopes
scope:
- "https://api.stripe.com/*"
- "https://dashboard.stripe.com/api/*"
# BAD: Too broad — would allow the secret to be sent anywhere on the domain
scope: "https://*"
# BAD: Includes unintended paths
scope: "https://api.github.com/repos"
# This won't match /repos/owner/name — you probably want /repos/*git-doc-mcp --manifest ./manifest.yml --secret GITHUB_TOKEN=ghp_abc123Multiple secrets:
git-doc-mcp --manifest ./manifest.yml \
--secret GITHUB_TOKEN=ghp_abc123 \
--secret API_KEY=sk-xyz789Secrets are automatically read from environment variables with the GIT_MCP_SECRET_ prefix:
export GIT_MCP_SECRET_GITHUB_TOKEN=ghp_abc123
export GIT_MCP_SECRET_API_KEY=sk-xyz789
git-doc-mcp --manifest ./manifest.ymlIf a required secret is not provided via flags or environment variables, the server refuses to start and exits with an error:
Error: Required secret "GITHUB_TOKEN" not provided
-
--secretflags (highest priority) -
GIT_MCP_SECRET_*environment variables - If still missing and
required: true— server exits with error
Access secrets via ctx.getSecret(name, url):
export default async function callApi(input, ctx) {
const url = `https://api.github.com/repos/${input.owner}/${input.repo}`;
// getSecret returns the value if:
// 1. The secret exists and was provided by the user
// 2. The URL matches the secret's scope pattern
// Otherwise returns undefined
const token = ctx.getSecret('GITHUB_TOKEN', url);
const headers = { 'Accept': 'application/vnd.github.v3+json' };
if (token) {
headers['Authorization'] = `token ${token}`;
}
const response = await ctx.fetch(url, { headers });
// ...
}-
getSecretalways takes TWO arguments: the secret name and the URL you'll use it with - Returns
undefinedif the secret wasn't provided, even if it's in the manifest - Returns
undefinedif the URL doesn't match the secret's scope, even if the secret was provided - All secret access attempts are audit-logged (including denials)
- Secret scopes are URL patterns for
ctx.getSecret(name, url)calls -- they are not affected by whether the action itself was loaded from a local file path or a remote URL. The scope validation always applies to theurlargument passed togetSecret.
Most tools should work without secrets (reduced functionality) and enhance with secrets:
export default async function search(input, ctx) {
const url = `https://api.github.com/search/code?q=${encodeURIComponent(input.query)}`;
const headers = { 'Accept': 'application/vnd.github.v3+json' };
const token = ctx.getSecret('GITHUB_TOKEN', url);
if (token) {
headers['Authorization'] = `token ${token}`;
ctx.log('debug', 'Authenticated (5000 req/hr limit)');
} else {
ctx.log('info', 'Unauthenticated (60 req/hr limit)');
}
const response = await ctx.fetch(url, { headers });
if (!response.ok && response.status === 403) {
return {
content: [{
type: 'text',
text: token
? 'Rate limit exceeded even with authentication.'
: 'Rate limit exceeded. Provide a GITHUB_TOKEN for higher limits.',
}],
isError: true,
};
}
// ...
}- Secrets are never passed to action scripts directly
-
ctx.getSecretvalidates the URL scope before returning the value - The sandbox cannot access secrets except through
ctx.getSecret - Cross-origin redirects strip Authorization and Cookie headers
Every getSecret call is logged:
- Secret name
- URL it was requested for
- Whether access was granted or denied
- Manifest name
Logs are written to ~/.git-doc-mcp/logs/audit.jsonl.