Autodiscovery Patterns: From RSS to MCP Manifests

Autodiscovery Patterns: From RSS to MCP Manifests

How the web's most successful discovery mechanisms inspired zero-config MCP server installation

David H. Friedel Jr.· 2026-04-01 ·MCP mcp-manifest

Introduction: Standing on the Shoulders of Web Standards

The Model Context Protocol (MCP) solves a critical problem: how AI assistants connect to external tools and data sources. But there's a gap. The protocol defines the handshake after connection — the initialize request, capability negotiation, tool invocation — but says nothing about everything that comes before: how to install the server, how to configure it, and how to wire your client to it.

Every MCP server today is a snowflake. One README says "run npm install -g, then paste this JSON into your Claude config." Another says "install via dotnet tool, set these environment variables, then add this block to your settings file." A third uses Docker. The information exists, but it's trapped in prose, requiring manual translation into client-specific configuration.

The web solved this problem decades ago. RSS feeds don't require you to manually configure your feed reader — you just paste a URL, and the reader discovers the feed automatically. OpenID lets you log in with a domain name, not a full OAuth endpoint URL. The pattern is simple: publish a machine-readable pointer at a predictable location, and let clients do the rest.

mcp-manifest.json brings this pattern to MCP servers. It's a single JSON file that describes how to install, configure, and connect to an MCP server. Ship it with your server, publish a pointer from your website, and any MCP client can set up your server from just your domain name.

This article is a deep dive into the autodiscovery mechanism — how it works, why it's designed this way, and how to implement it in your MCP client.

The Well-Known URL Pattern

The simplest and most direct discovery method is the well-known URL pattern. It's the same approach used by /.well-known/security.txt, /.well-known/change-password, and dozens of other web standards.

For MCP manifests, the well-known path is:

GET https://example.com/.well-known/mcp-manifest.json

If this URL returns a 200 OK response with Content-Type: application/json, the client parses the body as the manifest. Done.

This method is ideal for API servers and headless services that don't serve HTML. If your MCP server is part of a SaaS product with a REST API, the well-known URL is the natural fit. Your API already serves JSON at predictable paths — the manifest is just another endpoint.

Server Setup

Enabling well-known URL discovery is straightforward:

  1. Place the manifest file at /.well-known/mcp-manifest.json in your web server's document root.
  2. Set the Content-Type header to application/json.
  3. Enable CORS (optional but recommended) with Access-Control-Allow-Origin: * to allow cross-origin requests from browser-based MCP clients.

Example Nginx configuration:

location /.well-known/mcp-manifest.json {
    alias /var/www/mcp-manifest.json;
    add_header Content-Type application/json;
    add_header Access-Control-Allow-Origin *;
}

Example for Node.js/Express:

app.get('/.well-known/mcp-manifest.json', (req, res) => {
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.sendFile('/path/to/mcp-manifest.json');
});

Why This Works

The well-known URL pattern has three key advantages:

  1. No HTML parsing required. Clients can fetch and parse JSON directly.
  2. Works for APIs. Services that don't have a traditional website can still participate in autodiscovery.
  3. Predictable. Clients know exactly where to look — no guessing, no crawling.

If the well-known URL returns a 404, the client falls back to the next discovery method.

The second discovery method is the HTML link tag — the same mechanism RSS readers have used for 20+ years.

Server authors add a <link> tag to the <head> of their website:

<link rel="mcp-manifest" type="application/json" href="/mcp-manifest.json" />

The href attribute points to the manifest file. It can be:

  • Relative: href="/mcp-manifest.json"
  • Absolute: href="https://cdn.example.com/mcp-manifest.json"
  • Cross-domain: href="https://raw.githubusercontent.com/org/repo/main/mcp-manifest.json"

This method is ideal for MCP servers associated with a product website. If you have a landing page, documentation site, or company homepage, the link tag is the natural place to publish the manifest pointer.

Multiple Manifests on One Domain

A single page can contain multiple <link rel="mcp-manifest"> tags, each pointing to a different manifest. This is useful for organizations that publish multiple MCP servers.

Example from the codebase:

<link rel="mcp-manifest" type="application/json" 
      href="/mcp-manifests/analytics.json" 
      title="Analytics Server" />
<link rel="mcp-manifest" type="application/json" 
      href="/mcp-manifests/licensing.json" 
      title="Licensing Server" />

Clients should present all discovered manifests and let the user choose which server to install. The optional title attribute provides a hint for the UI.

Clients fetch the HTML page, parse the DOM (or use a regex for simple cases), and extract <link> tags with rel="mcp-manifest".

Example JavaScript implementation:

async function discoverManifestFromHTML(url) {
  const response = await fetch(url);
  const html = await response.text();
  
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');
  
  const links = doc.querySelectorAll('link[rel="mcp-manifest"]');
  const manifests = [];
  
  for (const link of links) {
    const href = link.getAttribute('href');
    const title = link.getAttribute('title') || null;
    const absoluteUrl = new URL(href, url).toString();
    manifests.push({ url: absoluteUrl, title });
  }
  
  return manifests;
}

Why This Works

The HTML link tag pattern has three advantages:

  1. Flexible hosting. The manifest can live anywhere — on the same server, a CDN, or a GitHub raw URL.
  2. Human-discoverable. Developers inspecting the page source can see the manifest link immediately.
  3. Battle-tested. RSS readers, podcast clients, and OpenID consumers have used this pattern for decades. It works.

The Client Resolution Algorithm

When a user provides input — typing "ironlicensing.com" into an "Add MCP Server" field — the client needs to resolve that input into a manifest. The resolution algorithm tries multiple strategies in order, stopping at the first success.

Here's the full algorithm from the spec:

INPUT: user_input (could be domain, URL, or file path)

1. If user_input is a local file path and the file exists:
   → Parse as manifest JSON. Done.

2. If user_input looks like a direct manifest URL (ends with .json):
   → Fetch and parse as manifest JSON. Done.

3. Normalize to a base URL:
   - If no scheme, prepend "https://"
   - If no path, use root "/"
   → base_url

4. Try well-known URL:
   → GET {base_url}/.well-known/mcp-manifest.json
   - If 200 with valid JSON manifest: Done.

5. Fetch the HTML page:
   → GET {base_url}
   - Parse HTML for <link rel="mcp-manifest"> tags
   - If found: fetch the href URL, parse as manifest. Done.

6. Discovery failed.
   → Inform user that no manifest was found at the given location.

Example: Full Discovery Flow

Let's walk through a real example. A developer wants to add the IronLicensing MCP server. They type ironlicensing.com into their MCP client.

Step 1: Client normalizes input to https://ironlicensing.com/.

Step 2: Client tries the well-known URL:

GET https://ironlicensing.com/.well-known/mcp-manifest.json
→ 200 OK

Step 3: Client parses the manifest:

{
  "server": {
    "name": "ironlicensing",
    "displayName": "IronLicensing",
    "description": "Manage IronLicensing products, tiers, features, licenses, and analytics"
  },
  "install": [{
    "method": "dotnet-tool",
    "package": "IronLicensing.Mcp",
    "command": "ironlicensing-mcp"
  }],
  "config": [
    {
      "key": "api-key",
      "type": "secret",
      "required": false,
      "prompt": "API key (or configure via add_account tool after connecting)"
    }
  ]
}

Step 4: Client checks if ironlicensing-mcp is on PATH → not found.

Step 5: Client shows: "Install IronLicensing MCP? Run: dotnet tool install -g IronLicensing.Mcp"

Step 6: User confirms → tool installed.

Step 7: Client prompts for config: "API key (sk_live_xxx)" → user enters key.

Step 8: Client writes to ~/.claude/settings.json:

{
  "mcpServers": {
    "ironlicensing": {
      "command": "ironlicensing-mcp",
      "args": ["--api-key", "sk_live_..."]
    }
  }
}

Step 9: Client verifies MCP handshake → success.

Total user effort: Type domain name, enter API key. Everything else is automated.

Implementation Tips

  1. Normalize input early. Users might type example.com, https://example.com, or https://example.com/. Normalize to a canonical base URL before attempting discovery.

  2. Set reasonable timeouts. Network requests can hang. Use a 5-10 second timeout for HTTP fetches.

  3. Handle redirects. Follow HTTP 3xx redirects when fetching the well-known URL or HTML page.

  4. Validate JSON. After fetching a manifest, validate it against the JSON Schema (https://mcp-manifest.dev/schema/v0.1.json). Fail gracefully if the manifest is malformed.

  5. Cache discovered manifests. If a user adds the same server to multiple projects, don't re-fetch the manifest every time. Cache it locally with a reasonable TTL (e.g., 1 hour).

Handling Multiple Manifests on One Domain

A single domain might host multiple MCP servers. For example, a SaaS company might offer separate servers for analytics, licensing, and customer support.

The HTML link tag method supports this natively — just include multiple <link> tags:

<link rel="mcp-manifest" type="application/json" 
      href="/manifests/analytics.json" 
      title="Analytics Server" />
<link rel="mcp-manifest" type="application/json" 
      href="/manifests/licensing.json" 
      title="Licensing Server" />
<link rel="mcp-manifest" type="application/json" 
      href="/manifests/support.json" 
      title="Support Automation" />

When a client discovers multiple manifests, it should:

  1. Parse all manifests. Fetch and validate each href URL.
  2. Present a choice. Show the user a list of available servers with their displayName and description.
  3. Allow multi-select. Let the user install one, some, or all of the discovered servers.

Example UI mockup:

┌─────────────────────────────────────────────────┐
│ Found 3 MCP servers at example.com:            │
│                                                 │
│ ☑ Analytics Server                             │
│   Real-time analytics and reporting            │
│                                                 │
│ ☑ Licensing Server                             │
│   Manage product tiers, features, and licenses │
│                                                 │
│ ☐ Support Automation                           │
│   Ticket management and customer support       │
│                                                 │
│            [Cancel]  [Install Selected]        │
└─────────────────────────────────────────────────┘

Well-Known URL Limitation

The well-known URL pattern (/.well-known/mcp-manifest.json) can only point to one manifest. If you need to publish multiple servers, use the HTML link tag method instead, or serve a manifest index at the well-known URL:

{
  "manifests": [
    {
      "name": "analytics",
      "url": "/manifests/analytics.json",
      "title": "Analytics Server"
    },
    {
      "name": "licensing",
      "url": "/manifests/licensing.json",
      "title": "Licensing Server"
    }
  ]
}

This is not part of the v0.1 spec, but it's a natural extension for future versions.

Security Considerations and CORS

Autodiscovery involves fetching JSON from arbitrary URLs. This introduces security considerations that client developers must handle carefully.

CORS (Cross-Origin Resource Sharing)

Browser-based MCP clients (e.g., a web app that connects to MCP servers) will encounter CORS restrictions when fetching manifests from third-party domains.

Server authors should include CORS headers when serving manifests:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type

This allows any origin to fetch the manifest. Since manifests are public metadata (not user data), broad CORS access is appropriate.

Client developers should handle CORS errors gracefully. If a manifest fetch fails due to CORS, inform the user and suggest:

  1. Using a desktop client (which doesn't have CORS restrictions).
  2. Asking the server author to enable CORS.
  3. Manually downloading the manifest and providing it as a local file.

HTTPS Enforcement

Clients should prefer HTTPS when normalizing user input. If a user types example.com, normalize to https://example.com, not http://.

If the HTTPS request fails, clients MAY fall back to HTTP, but should warn the user:

⚠️  Warning: Fetching manifest over insecure HTTP.
   Manifests may contain sensitive configuration details.
   Proceed only if you trust this source.

Manifest Validation

After fetching a manifest, clients MUST validate it against the JSON Schema before using it. This prevents malformed or malicious manifests from causing client errors.

Example validation using a JavaScript library:

import Ajv from 'ajv';

const ajv = new Ajv();
const schema = await fetch('https://mcp-manifest.dev/schema/v0.1.json').then(r => r.json());
const validate = ajv.compile(schema);

const manifest = await fetch(manifestUrl).then(r => r.json());

if (!validate(manifest)) {
  console.error('Invalid manifest:', validate.errors);
  throw new Error('Manifest validation failed');
}

Arbitrary Code Execution

Manifests describe how to install and run software. This is inherently powerful — and potentially dangerous.

Clients should:

  1. Show the install command before executing it. Let the user review what will run.
  2. Require user confirmation before installing anything.
  3. Sandbox execution if possible (e.g., Docker, VM, or restricted subprocess).
  4. Log all actions (install commands, config writes) for auditability.

Example confirmation prompt:

┌─────────────────────────────────────────────────┐
│ Install IronLicensing MCP Server?              │
│                                                 │
│ This will run:                                  │
│   dotnet tool install -g IronLicensing.Mcp \   │
│     --add-source https://git.marketally.com/...│
│                                                 │
│ Proceed?                  [Cancel]  [Install]  │
└─────────────────────────────────────────────────┘

Secret Handling

Manifests declare configuration parameters typed as secret (e.g., API keys). Clients must handle these with care:

  1. Mask display. Show sk_live_•••••••• instead of the full key in UI.
  2. Don't log. Never write secrets to log files or console output.
  3. Encrypt at rest. Store secrets in the system keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) rather than plain text config files.

Trust and Verification

The v0.1 spec does not include cryptographic signatures or a trust model. Clients rely on HTTPS and DNS for authenticity — the same trust model as the rest of the web.

Future versions may add:

  • Signed manifests (e.g., using PGP or Sigstore).
  • Manifest hashes published via DNS TXT records.
  • A central registry that vets and vouches for manifests.

For now, clients should treat manifests like any other web resource: trust HTTPS, validate content, and let users make informed decisions.

Implementation Guide for Client Developers

You're building an MCP client — an AI assistant, IDE plugin, or CLI tool. You want to support manifest autodiscovery so users can add MCP servers with minimal friction. Here's how to implement it.

Step 1: Accept User Input

Add a UI element where users can provide a domain, URL, or file path. Examples:

  • GUI: A text field labeled "Add MCP Server" with placeholder text: example.com or /path/to/manifest.json
  • CLI: A command like mcp add <input>

Don't force users to provide a full URL. Accept flexible input:

  • ironlicensing.com → normalize to https://ironlicensing.com/
  • https://example.com/mcp-manifest.json → use directly
  • /home/user/manifest.json → load from file system

Step 2: Implement the Resolution Algorithm

Follow the algorithm from the spec (see "The Client Resolution Algorithm" section above). Here's pseudocode:

def resolve_manifest(user_input: str) -> dict:
    # Step 1: Check if it's a local file
    if os.path.isfile(user_input):
        return json.load(open(user_input))
    
    # Step 2: Check if it's a direct URL to a .json file
    if user_input.endswith('.json'):
        return fetch_json(user_input)
    
    # Step 3: Normalize to base URL
    base_url = normalize_url(user_input)
    
    # Step 4: Try well-known URL
    well_known_url = f"{base_url}/.well-known/mcp-manifest.json"
    try:
        return fetch_json(well_known_url)
    except HTTPError:
        pass
    
    # Step 5: Fetch HTML and parse link tags
    html = fetch(base_url)
    manifests = parse_link_tags(html, base_url)
    
    if len(manifests) == 0:
        raise DiscoveryError("No manifest found")
    
    if len(manifests) == 1:
        return fetch_json(manifests[0]['url'])
    
    # Multiple manifests: let user choose
    chosen = prompt_user_to_choose(manifests)
    return fetch_json(chosen['url'])

Step 3: Validate the Manifest

Use the JSON Schema to validate the manifest structure:

import jsonschema

schema = fetch_json('https://mcp-manifest.dev/schema/v0.1.json')
jsonschema.validate(instance=manifest, schema=schema)

If validation fails, show the user a clear error message and abort.

Step 4: Check Installation Status

Before prompting the user to install, check if the server is already available:

command = manifest['install'][0]['command']
if shutil.which(command):
    print(f"✓ {command} is already installed")
else:
    print(f"✗ {command} not found. Install required.")

Step 5: Install the Server

Present the install command and ask for confirmation:

install = manifest['install'][0]
method = install['method']
package = install['package']

if method == 'npm':
    cmd = f"npm install -g {package}"
elif method == 'dotnet-tool':
    source = install.get('source', '')
    cmd = f"dotnet tool install -g {package}"
    if source:
        cmd += f" --add-source {source}"
elif method == 'pip':
    cmd = f"pip install {package}"

if confirm(f"Run: {cmd}"):
    subprocess.run(cmd, shell=True, check=True)

Step 6: Prompt for Configuration

Walk through each config entry in the manifest:

config_values = {}

for param in manifest.get('config', []):
    key = param['key']
    prompt_text = param.get('prompt', param['description'])
    param_type = param['type']
    required = param.get('required', False)
    default = param.get('default')
    
    if param_type == 'secret':
        value = prompt_password(prompt_text)
    elif param_type == 'path':
        value = prompt_file_picker(prompt_text)
    else:
        value = prompt_text_input(prompt_text, default=default)
    
    if required and not value:
        raise ValueError(f"Required parameter '{key}' not provided")
    
    config_values[key] = value

Step 7: Generate Client Configuration

Use the settings_template to build the final configuration:

template = manifest.get('settings_template', {})
command = template.get('command', manifest['install'][0]['command'])
args = template.get('args', [])

# Substitute variables
args = [arg.replace(f"${{{key}}}", value) for arg in args for key, value in config_values.items()]

client_config = {
    manifest['server']['name']: {
        'command': command,
        'args': args
    }
}

Step 8: Write to Settings File

Merge the generated config into the client's settings file:

settings_path = os.path.expanduser('~/.mcp-client/settings.json')
settings = json.load(open(settings_path))
settings['mcpServers'].update(client_config)
json.dump(settings, open(settings_path, 'w'), indent=2)

Step 9: Verify Connection

Attempt an MCP handshake to verify the server is working:

process = subprocess.Popen(
    [command] + args,
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE
)

# Send MCP initialize request
init_request = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {...}}
process.stdin.write(json.dumps(init_request).encode() + b'\n')
process.stdin.flush()

# Read response
response = json.loads(process.stdout.readline())
if 'result' in response:
    print("✓ MCP server connected successfully")
else:
    print("✗ Connection failed:", response.get('error'))

Step 10: Provide Feedback

Show the user a summary of what happened:

✓ Installed ironlicensing-mcp via dotnet tool
✓ Configured API key
✓ Added to ~/.mcp-client/settings.json
✓ Verified MCP connection

You can now use IronLicensing commands in your assistant.

Testing Your Implementation

Use the example manifests in the codebase to test your client:

  • Minimal: examples/minimal.json — simplest possible manifest
  • SQLite: examples/sqlite.json — npm install, file path config
  • GitHub: examples/github.json — secret token handling
  • IronLicensing: examples/ironlicensing.json — dotnet tool, custom registry, optional config

Test edge cases:

  • Malformed JSON
  • Missing required fields
  • Network timeouts
  • CORS errors (for browser clients)
  • Multiple manifests on one domain
  • Manifests hosted on CDNs or GitHub raw URLs

Optimizations

  1. Cache discovered manifests to avoid redundant fetches.
  2. Parallelize network requests (well-known URL + HTML fetch) to reduce latency.
  3. Pre-validate input (e.g., reject obviously invalid domains) before attempting discovery.
  4. Show progress indicators during long-running operations (install, network fetch).

With these steps, your MCP client will support the full autodiscovery flow — from domain name to connected server — with minimal user friction.

Back to Blog