Configuration Architecture: Types, Secrets, and Templates

Configuration Architecture: Types, Secrets, and Templates

How mcp-manifest.json handles typed fields, secret masking, environment variables, and dynamic settings generation for zero-config MCP server setup.

David H. Friedel Jr.· 2026-04-01 ·mcp-manifest secrets templates types

Introduction: Configuration is Complex

Every MCP server has configuration needs — API keys, database paths, base URLs, feature flags. Today, these requirements live in README files as prose instructions: "Set the GITHUB_TOKEN environment variable to your personal access token" or "Pass --db-path with the path to your SQLite file."

Users must manually translate these instructions into their MCP client's configuration format, which varies by client. The result? Copy-paste errors, misconfigured servers, and support requests.

mcp-manifest.json solves this with a structured configuration system that declares:

  • What parameters the server needs (key, description)
  • How values are typed and validated (type)
  • Where values come from (env_var, arg, default)
  • How clients should present them (prompt, required)
  • How to build the final settings object (settings_template)

This article walks through the six configuration types, secret management, resolution order, and the template system that generates client configuration automatically.

The Six Configuration Types Explained

The config array in mcp-manifest.json supports six value types, each with distinct validation rules and UI hints:

1. string — Free-form text

Generic text input. Use for names, identifiers, or any unstructured value.

{
  "key": "profile",
  "description": "Named account profile from ~/.ironlicensing/config.json",
  "type": "string",
  "required": false,
  "arg": "--profile",
  "prompt": "Account profile (leave empty for default)"
}

Clients present a text input field. No format validation.

2. boolean — True/false

Binary flags for feature toggles or mode switches.

{
  "key": "verbose",
  "description": "Enable verbose logging",
  "type": "boolean",
  "default": false,
  "arg": "--verbose"
}

Clients present a checkbox or toggle switch.

3. number — Numeric value

Integers or floats for ports, timeouts, limits, or counts.

{
  "key": "port",
  "description": "HTTP server port",
  "type": "number",
  "default": 8080,
  "arg": "--port"
}

Clients validate numeric format and may show a number spinner.

4. path — Filesystem path

File or directory paths. Clients should show a file picker or path browser.

{
  "key": "db-path",
  "description": "Path to SQLite database file",
  "type": "path",
  "required": true,
  "prompt": "Database file path"
}

From the SQLite example:

"settings_template": {
  "command": "mcp-server-sqlite",
  "args": ["${db-path}"]
}

The path type signals that this is a filesystem reference, not just a string.

5. url — Web URL

HTTP/HTTPS endpoints. Clients should validate URL format.

{
  "key": "base-url",
  "description": "IronLicensing API base URL",
  "type": "url",
  "required": false,
  "default": "http://localhost:5000",
  "env_var": "IRONLICENSING_BASE_URL",
  "arg": "--base-url",
  "prompt": "API base URL"
}

Clients can validate the scheme, check reachability, or warn about http:// in production.

6. secret — Sensitive value

API keys, tokens, passwords. The most important type for security.

{
  "key": "github-token",
  "description": "GitHub personal access token",
  "type": "secret",
  "required": true,
  "env_var": "GITHUB_TOKEN",
  "prompt": "GitHub personal access token (ghp_...)"
}

From the GitHub example manifest:

"config": [
  {
    "key": "github-token",
    "description": "GitHub personal access token",
    "type": "secret",
    "required": true,
    "env_var": "GITHUB_TOKEN",
    "prompt": "GitHub personal access token (ghp_...)"
  }
]

Clients must handle secret types specially (see next section).


Why types matter:

The type field transforms generic "configuration" into structured, validatable data. Clients can:

  • Show appropriate UI (file picker for path, password field for secret)
  • Validate format before writing config (URL syntax, numeric range)
  • Provide better error messages ("Expected number, got 'abc'")
  • Apply security policies (never log secret values)

The config array transforms README instructions into machine-readable, type-safe parameter declarations that clients can validate and present with appropriate UI.

Secret Management: Masking and Security

The secret type is a first-class security primitive in the manifest spec. It signals that a value is sensitive and must be handled with care.

Client Requirements for secret Fields

From the spec:

Clients SHOULD mask display, SHOULD NOT log

When a config parameter has "type": "secret":

  1. Input masking — Show ••••••• or **** instead of plaintext during entry
  2. Display masking — Never show the full value in UI (show sk_live_••••1234 or similar)
  3. Log exclusion — Omit from debug logs, error messages, and telemetry
  4. Storage security — Store in OS keychain or encrypted config, not plaintext JSON (client-dependent)
  5. No clipboard — Avoid auto-copying secrets to clipboard

Example: IronLicensing API Key

From examples/ironlicensing.json:

{
  "key": "api-key",
  "description": "IronLicensing API key (sk_live_xxx) from /app/settings/api-keys",
  "type": "secret",
  "required": false,
  "env_var": "IRONLICENSING_API_KEY",
  "arg": "--api-key",
  "prompt": "API key (or configure via add_account tool after connecting)"
}

When a client prompts for this value:

  • Prompt text: "API key (or configure via add_account tool after connecting)"
  • Input field: Password-style, masked as user types
  • Stored value: Written to environment variable IRONLICENSING_API_KEY or passed as --api-key arg
  • Never logged: If connection fails, error messages show --api-key=<redacted>, not the actual key

Why Environment Variables for Secrets?

Notice that both the GitHub and IronLicensing examples use env_var for secrets:

"env_var": "GITHUB_TOKEN"
"env_var": "IRONLICENSING_API_KEY"

This is best practice because:

  1. Separation of code and config — Secrets live outside the settings file
  2. OS-level security — Environment variables can be managed by system keychains, secret managers, or CI/CD platforms
  3. No accidental commits — Settings files can be version-controlled without exposing secrets
  4. Process isolation — Secrets are scoped to the process, not written to disk

Clients that support secret storage (like macOS Keychain or Windows Credential Manager) can intercept secret types and store them securely, then inject the environment variable at runtime.

Fallback: CLI Arguments

If environment variables aren't set, the arg field provides a fallback:

"arg": "--api-key"

The client can pass the secret as a command-line argument when spawning the MCP server process. This is less secure (arguments are visible in process lists) but works in environments where environment variables are difficult to manage.


Secret masking isn't just a nice-to-have — it's a specification requirement that prevents API keys from leaking into logs, screenshots, and error messages.

Resolution Order: env_var, arg, and default

A single config parameter can specify multiple sources for its value. The spec defines a resolution order that clients must follow:

Resolution Algorithm

From the spec:

Clients SHOULD resolve config values in this order:

  1. User-provided value (via UI prompt or manual entry)
  2. Environment variable (env_var field)
  3. CLI argument (arg field)
  4. Default value (default field)

Let's walk through an example with the IronLicensing base-url parameter:

{
  "key": "base-url",
  "description": "IronLicensing API base URL",
  "type": "url",
  "required": false,
  "default": "http://localhost:5000",
  "env_var": "IRONLICENSING_BASE_URL",
  "arg": "--base-url",
  "prompt": "API base URL"
}

Scenario 1: User Override

User manually enters https://api.ironlicensing.com in the client UI.

Result: Use https://api.ironlicensing.com (user-provided value takes precedence)

Scenario 2: Environment Variable Set

No user input, but IRONLICENSING_BASE_URL=https://staging.ironlicensing.com is set in the environment.

Result: Use https://staging.ironlicensing.com (environment variable is second priority)

Scenario 3: CLI Argument

No user input, no environment variable, but the manifest specifies "arg": "--base-url".

Result: Client passes --base-url as a command-line argument when spawning the server. The server itself reads the argument.

(Note: This requires the server to support the argument. The manifest documents what the server accepts, not what the client must provide.)

Scenario 4: Default Value

No user input, no environment variable, no argument.

Result: Use http://localhost:5000 (the default value)

Scenario 5: No Value, Required Field

If required: true and none of the above sources provide a value:

Result: Client must prompt the user or fail configuration with an error.


Why This Order?

The resolution order follows the principle of increasing specificity:

  1. User input — Most specific, explicit intent
  2. Environment variable — Session-specific, often set by deployment tools
  3. CLI argument — Server-specific, documented in the manifest
  4. Default — Least specific, fallback for common cases

This design supports multiple workflows:

  • Interactive setup: User enters values via UI prompts
  • CI/CD deployment: Environment variables injected by orchestration tools
  • Local development: Defaults work out-of-the-box for common cases (like localhost:5000)

Practical Example: Multi-Environment Setup

A developer working on three environments:

  • Local: Uses default http://localhost:5000
  • Staging: Sets IRONLICENSING_BASE_URL=https://staging.ironlicensing.com in .zshrc
  • Production: Client prompts for https://api.ironlicensing.com during setup

The same manifest supports all three scenarios without modification.

Building Dynamic Settings Templates

The settings_template field is where the manifest generates actual client configuration. It's a JSON object that gets merged into the client's MCP server settings, with variable substitution from the config array.

Template Syntax

Variables use ${key} syntax, where key matches a config[].key value.

Example 1: Simple Argument Substitution (SQLite)

From examples/sqlite.json:

"config": [
  {
    "key": "db-path",
    "description": "Path to SQLite database file",
    "type": "path",
    "required": true,
    "prompt": "Database file path"
  }
],
"settings_template": {
  "command": "mcp-server-sqlite",
  "args": ["${db-path}"]
}

User input: /home/user/data.db

Generated settings:

{
  "mcpServers": {
    "sqlite": {
      "command": "mcp-server-sqlite",
      "args": ["/home/user/data.db"]
    }
  }
}

The client substitutes ${db-path} with the user-provided value and writes the result to the settings file.

Example 2: Profile-Based Configuration (IronLicensing)

From examples/ironlicensing.json:

"config": [
  {
    "key": "profile",
    "description": "Named account profile from ~/.ironlicensing/config.json",
    "type": "string",
    "required": false,
    "arg": "--profile",
    "prompt": "Account profile (leave empty for default)"
  }
],
"settings_template": {
  "command": "ironlicensing-mcp",
  "args": ["--profile", "${profile}"]
}

User input: production

Generated settings:

{
  "mcpServers": {
    "ironlicensing": {
      "command": "ironlicensing-mcp",
      "args": ["--profile", "production"]
    }
  }
}

The --profile flag is passed as a command-line argument when the client spawns the server process.

Example 3: Environment Variables (GitHub)

From examples/github.json:

"config": [
  {
    "key": "github-token",
    "description": "GitHub personal access token",
    "type": "secret",
    "required": true,
    "env_var": "GITHUB_TOKEN",
    "prompt": "GitHub personal access token (ghp_...)"
  }
],
"settings_template": {
  "command": "mcp-server-github",
  "args": []
}

User input: ghp_abc123xyz

Generated settings:

{
  "mcpServers": {
    "github": {
      "command": "mcp-server-github",
      "args": [],
      "env": {
        "GITHUB_TOKEN": "ghp_abc123xyz"
      }
    }
  }
}

(Note: The exact format depends on the client. Some clients may write the environment variable to a separate .env file or system keychain instead of embedding it in the settings JSON.)

Why Templates?

The settings_template system bridges the gap between abstract configuration parameters and the concrete JSON blob that must be written to a client's settings file.

Without templates, clients would need to guess how to format arguments:

  • Does the API key go in args or env?
  • Is the database path positional or named (--db-path)?
  • Are boolean flags passed as --verbose true or just --verbose?

The template documents the exact format the server expects, eliminating ambiguity.

Advanced: Conditional Arguments

Some servers only accept arguments when certain conditions are met. For example, the IronLicensing server supports either a profile name or explicit API key + base URL.

The manifest can't express complex conditionals, so it documents the most common case (profile-based):

"settings_template": {
  "command": "ironlicensing-mcp",
  "args": ["--profile", "${profile}"]
}

Clients that support advanced configuration can detect when profile is empty and prompt for api-key and base-url instead, generating:

{
  "command": "ironlicensing-mcp",
  "args": ["--api-key", "sk_live_xxx", "--base-url", "https://api.ironlicensing.com"]
}

This is a client enhancement, not a spec requirement. The template provides a sensible default; sophisticated clients can do more.

Multi-Account and Profile Support

Many MCP servers need to support multiple accounts or environments. The manifest spec handles this through the profile pattern, demonstrated in the IronLicensing example.

The Profile Pattern

Instead of hardcoding credentials in the manifest, servers can accept a profile name that references a separate configuration file.

From examples/ironlicensing.json:

{
  "key": "profile",
  "description": "Named account profile from ~/.ironlicensing/config.json",
  "type": "string",
  "required": false,
  "arg": "--profile",
  "prompt": "Account profile (leave empty for default)"
}

The server reads credentials from ~/.ironlicensing/config.json:

{
  "profiles": {
    "default": {
      "api_key": "sk_live_default123",
      "base_url": "https://api.ironlicensing.com"
    },
    "staging": {
      "api_key": "sk_test_staging456",
      "base_url": "https://staging.ironlicensing.com"
    },
    "local": {
      "api_key": "sk_dev_local789",
      "base_url": "http://localhost:5000"
    }
  }
}

The manifest's settings_template passes the profile name:

"settings_template": {
  "command": "ironlicensing-mcp",
  "args": ["--profile", "${profile}"]
}

User workflow:

  1. User types ironlicensing.com into their MCP client
  2. Client discovers the manifest, installs the tool
  3. Client prompts: "Account profile (leave empty for default)"
  4. User enters: staging
  5. Client writes:
    {
      "mcpServers": {
        "ironlicensing": {
          "command": "ironlicensing-mcp",
          "args": ["--profile", "staging"]
        }
      }
    }
    
  6. When the server starts, it reads ~/.ironlicensing/config.json and loads the staging profile

Why Profiles?

Separation of concerns:

  • Manifest — Describes how to configure the server (what parameters exist)
  • Profile file — Stores actual credentials (API keys, URLs)
  • Client settings — References a profile by name

This design supports:

  • Multiple accounts — Switch between personal, work, client-a profiles
  • Environment isolationdev, staging, prod profiles with different API keys
  • Team sharing — Manifest is public, profile file is private
  • Credential rotation — Update the profile file without touching client settings

Alternative: Direct Credential Entry

The IronLicensing manifest also supports direct credential entry for users who don't want to manage a profile file:

{
  "key": "api-key",
  "description": "IronLicensing API key (sk_live_xxx) from /app/settings/api-keys",
  "type": "secret",
  "required": false,
  "env_var": "IRONLICENSING_API_KEY",
  "arg": "--api-key",
  "prompt": "API key (or configure via add_account tool after connecting)"
}

Clients can prompt for api-key and base-url instead of profile, generating:

{
  "command": "ironlicensing-mcp",
  "args": ["--api-key", "sk_live_xxx", "--base-url", "https://api.ironlicensing.com"]
}

The manifest supports both patterns, letting users choose their workflow.

Multi-Instance Configuration

Some users need multiple instances of the same server (e.g., connecting to two different GitHub accounts).

Clients can support this by:

  1. Discovering the manifest once
  2. Prompting for instance-specific config (e.g., "GitHub Account 1", "GitHub Account 2")
  3. Writing multiple entries to mcpServers:
{
  "mcpServers": {
    "github-personal": {
      "command": "mcp-server-github",
      "env": { "GITHUB_TOKEN": "ghp_personal123" }
    },
    "github-work": {
      "command": "mcp-server-github",
      "env": { "GITHUB_TOKEN": "ghp_work456" }
    }
  }
}

The manifest doesn't need to change — the client handles multi-instance logic.

Best Practices for Server Authors

1. Always Declare type for Config Parameters

Don't use generic string when a more specific type applies:

Bad:

{
  "key": "api-key",
  "type": "string"
}

Good:

{
  "key": "api-key",
  "type": "secret"
}

This enables clients to mask the value and apply security policies.

2. Use env_var for Secrets

Avoid passing secrets as command-line arguments (they're visible in process lists).

Bad:

{
  "key": "api-key",
  "type": "secret",
  "arg": "--api-key"
}

Good:

{
  "key": "api-key",
  "type": "secret",
  "env_var": "MY_API_KEY"
}

Clients can inject the environment variable at runtime without exposing it in process lists.

3. Provide Sensible Defaults

If a parameter has a common value, declare it as default:

{
  "key": "base-url",
  "type": "url",
  "default": "http://localhost:5000"
}

This enables zero-config local development.

4. Write Clear Prompts

The prompt field is what users see during setup. Make it actionable:

Bad:

"prompt": "API key"

Good:

"prompt": "GitHub personal access token (ghp_...) — get one at https://github.com/settings/tokens"

Include:

  • Format hint(ghp_...), (sk_live_xxx)
  • Where to get it — Link to settings page or docs
  • What it's used for — "Read access to repositories"

5. Use Profiles for Multi-Account Scenarios

If your server supports multiple accounts, implement the profile pattern:

{
  "key": "profile",
  "description": "Named account profile from ~/.myserver/config.json",
  "type": "string",
  "required": false,
  "arg": "--profile",
  "prompt": "Account profile (leave empty for default)"
}

This keeps credentials out of the manifest and client settings.

6. Document the Settings Template

The settings_template is the contract between your server and MCP clients. Test it:

  1. Generate settings using the template
  2. Spawn your server with those settings
  3. Verify it connects and initializes successfully

If the template is wrong, no client can configure your server correctly.

7. Mark Optional Parameters as required: false

Don't make everything required. If a parameter has a default or can be omitted:

{
  "key": "timeout",
  "type": "number",
  "required": false,
  "default": 30
}

This reduces friction during initial setup.

8. Test Autodiscovery

After publishing your manifest:

  1. Serve it at /.well-known/mcp-manifest.json on your website
  2. Add <link rel="mcp-manifest"> to your HTML
  3. Test discovery with a client or curl:
curl https://yoursite.com/.well-known/mcp-manifest.json

Verify the JSON is valid and matches the schema.

9. Version Your Manifest

When you update your server's configuration requirements:

  1. Bump server.version in the manifest
  2. Keep the manifest backward-compatible (add new fields, don't remove old ones)
  3. Document breaking changes in release notes

Clients may cache manifests — ensure old versions still work.

10. Validate Against the Schema

Use the JSON Schema to catch errors before publishing:

{
  "$schema": "https://mcp-manifest.dev/schema/v0.1.json"
}

Most editors (VS Code, IntelliJ) will validate automatically and show errors inline.


Summary

The config array and settings_template system transform MCP server configuration from manual README instructions to machine-readable, type-safe declarations.

Key takeaways:

  • Six typesstring, boolean, number, path, url, secret
  • Secret masking — Clients must never log or display secret values in plaintext
  • Resolution order — User input → env_var → arg → default
  • Templates${key} syntax generates client settings automatically
  • Profiles — Support multi-account scenarios without hardcoding credentials
  • Best practices — Use specific types, provide defaults, write clear prompts

With a well-crafted manifest, users can install and configure your MCP server in seconds — no copy-paste, no README archaeology, no support tickets.

Back to Blog