How Claude Desktop finds servers, the anatomy of configuration files, package-based installation patterns, tool namespacing, and everything you need to publish a discoverable MCP server.
MCP servers don't broadcast themselves. There is no automatic detection, no mDNS, no service registry running in the background. Every host application needs to be explicitly told where to find its servers — and this gap is exactly what the discovery layer solves.
When Claude Desktop launches, it faces a fundamental question: what tools does the user want me to have? The answer lives entirely in a configuration file. Without that file, Claude has no tools, no resources, no prompts — just the base language model. With it, Claude can suddenly search GitHub, query databases, send Slack messages, and read your file system.
This might feel like a limitation, but it is actually a deliberate security design. Explicit configuration means:
The current ecosystem has three distinct discovery mechanisms, each suited to different deployment contexts:
By the end of today you will understand all three, know which to use when, and be able to publish a server that any host can discover and run.
The claude_desktop_config.json file is the primary discovery mechanism for Claude Desktop. Understanding every field unlocks full control over which servers load, how they authenticate, and what resources they consume.
The file lives at different paths per OS:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux | ~/.config/Claude/claude_desktop_config.json |
Here is a fully annotated config with every supported option:
{
"mcpServers": {
// ── Stdio server (local process) ──────────────────────────
"github": {
"command": "npx", // executable to run
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
}
},
// ── Python / uvx server ───────────────────────────────────
"filesystem": {
"command": "uvx",
"args": ["mcp-server-filesystem", "/home/user/projects"],
"env": {}
},
// ── Local TypeScript project (dev mode) ───────────────────
"my-server": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"NODE_ENV": "production",
"API_KEY": "secret-goes-here"
}
},
// ── HTTP / Streamable HTTP remote server ──────────────────
"remote-analytics": {
"url": "https://mcp.example.com/mcp", // uses Streamable HTTP
"headers": {
"Authorization": "Bearer eyJ..."
}
},
// ── Docker-based server ───────────────────────────────────
"postgres": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "DATABASE_URL",
"mcp/postgres:latest"
],
"env": {
"DATABASE_URL": "postgresql://user:pass@host:5432/db"
}
}
}
}
command + args for stdio servers (local process). Use url for remote HTTP/Streamable HTTP servers. You cannot mix them in one entry. For stdio servers, the process is spawned by Claude Desktop; for URL servers, Claude Desktop connects as an HTTP client.After editing the config, Claude Desktop must be fully restarted (not just reloaded) to pick up changes. There is currently no hot-reload mechanism.
The config format differs depending on transport. Understanding these differences helps you choose the right deployment model and debug connection failures quickly.
command + argsproxy_buffering offurl → /sse endpointheaders fieldurl → /mcp endpointconfig examples — all three transports// 1. stdio — starts node process locally "my-tool": { "command": "node", "args": ["/Users/you/my-mcp-server/dist/index.js"], "env": { "LOG_LEVEL": "info" } } // 2. HTTP+SSE — legacy, points to /sse endpoint "my-remote-v1": { "url": "https://mcp.example.com/sse" } // 3. Streamable HTTP — points to /mcp endpoint, with auth "my-remote-v2": { "url": "https://mcp.example.com/mcp", "headers": { "Authorization": "Bearer ${MY_TOKEN}" } }
${VAR_NAME} syntax in the config. Claude Desktop (as of early 2026) does not interpolate env vars in the headers field — values must be literal strings. For stdio servers, the env object passes variables to the spawned process environment, so you can reference them in server code via process.env.VAR_NAME.The most user-friendly MCP servers ship as installable packages. Instead of cloning a repo and running npm install && npm run build, users run a single command or add a one-liner to their config. This is the direction the ecosystem is heading.
The npx -y pattern downloads and executes an npm package without a permanent install. Claude Desktop uses this constantly for official servers:
claude_desktop_config.json"brave-search": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": { "BRAVE_API_KEY": "BSAxxxxxxx" }
}
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx" }
}
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"]
}
The -y flag auto-accepts the install prompt. Without it, the process blocks waiting for user confirmation — the server never starts.
uvx is the Python equivalent — it uses uv's tool runner to execute a Python package in an isolated environment without polluting global Python. Perfect for Python-based MCP servers:
claude_desktop_config.json — Python servers via uvx"filesystem": {
"command": "uvx",
"args": ["mcp-server-filesystem", "/Users/you/Documents"]
}
"sqlite": {
"command": "uvx",
"args": ["mcp-server-sqlite", "--db-path", "/tmp/mydb.db"]
}
"git": {
"command": "uvx",
"args": ["mcp-server-git", "--repository", "/path/to/repo"]
}
uvx is not installed, run curl -LsSf https://astral.sh/uv/install.sh | sh.To make your server installable via npx, you need the right package.json shape:
package.json — publishable MCP server{
"name": "@yourscope/mcp-server-weather",
"version": "1.0.0",
"description": "MCP server for weather data via OpenWeatherMap",
"type": "module",
"bin": {
"mcp-server-weather": "./dist/index.js"
},
"main": "./dist/index.js",
"files": ["dist/"],
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"prepublishOnly": "npm run build"
},
"keywords": ["mcp", "mcp-server", "weather", "openweathermap"],
"engines": { "node": ">=18" },
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.22.0"
},
"devDependencies": {
"typescript": "^5.3.0"
}
}
The bin field is critical — it tells npm what executable to create when installed. Without it, npx has nothing to run. The entry point must have a shebang: #!/usr/bin/env node.
dist/index.js — entry point with shebang#!/usr/bin/env node // This line makes the file executable via npx / node directly import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; const server = new McpServer({ name: 'weather', version: '1.0.0' }); server.tool( 'get_weather', { location: z.string().describe('City name or coordinates') }, async ({ location }) => { const apiKey = process.env.OPENWEATHER_API_KEY; if (!apiKey) throw new Error('OPENWEATHER_API_KEY not set'); const res = await fetch( `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(location)}&appid=${apiKey}&units=metric` ); const data = await res.json() as any; return { content: [{ type: 'text', text: `${location}: ${data.main.temp}°C, ${data.weather[0].description}` }] }; } ); const transport = new StdioServerTransport(); await server.connect(transport);
Once published, users can add your server with just:
"weather": {
"command": "npx",
"args": ["-y", "@yourscope/mcp-server-weather"],
"env": { "OPENWEATHER_API_KEY": "abc123" }
}
Every MCP connection starts with an initialize request–response exchange. This is your server's moment to declare its identity, its protocol version, and exactly which capabilities it supports. Get this right and hosts can discover your features automatically.
The initialize response shape, with all capability fields:
initialize response — complete shape{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", // latest spec version "serverInfo": { "name": "weather", // short kebab-case name "version": "1.0.0" // semver }, "capabilities": { "tools": { "listChanged": true // server can push tool list changes }, "resources": { "subscribe": true, // clients can subscribe to resource updates "listChanged": true }, "prompts": { "listChanged": true }, "logging": {}, // server can send log messages to client "experimental": { "progressReporting": true // optional experimental features } } } }
When using the TypeScript SDK's McpServer, capabilities are inferred automatically based on what you register. If you call server.tool(), the SDK sets capabilities.tools. You only need to declare capabilities explicitly when using the low-level Server class:
TypeScript SDK — explicit capability declaration (low-level)import { Server } from '@modelcontextprotocol/sdk/server/index.js';
const server = new Server(
{ name: 'my-server', version: '2.1.0' },
{
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
prompts: { listChanged: true },
logging: {}
}
}
);
initialize request; the server responds with its own. The intersection of both determines what features are available for the session. If the server advertises resources.subscribe but the client does not support subscriptions, subscription calls will be ignored or fail gracefully.The client sends "protocolVersion": "2025-03-26" (or whatever it supports). The server must respond with a version it also supports. If there is no overlap, the connection must be closed. The SDK handles this for you when using McpServer.
| Version | Release | Key additions |
|---|---|---|
| 2024-11-05 | Nov 2024 | Initial release. stdio + HTTP+SSE transports, Tools/Resources/Prompts primitives |
| 2025-03-26 | Mar 2025 | Streamable HTTP transport, resumable events, request batching, enhanced progress |
When a host loads multiple MCP servers simultaneously, their tools all end up in the same namespace from the AI's perspective. If two servers both expose a tool named search, the AI has no way to distinguish them — and the host has to pick one or fail.
Good tool naming eliminates ambiguity before it starts. Here are the conventions that have emerged from the MCP ecosystem:
Example: Two servers with naive naming cause a collision. Good naming prevents it.
Naming rules that the MCP community follows:
| Rule | Example (good) | Example (bad) |
|---|---|---|
| Prefix with service name | github_create_pr | create_pr |
| snake_case only | slack_send_message | slackSendMessage |
| Verb_noun pattern | db_query_table | db_table |
| Max 64 characters | github_get_pull_request | github_fetch_the_current_pull_request_data_from_api |
| Avoid generic verbs alone | notion_search_pages | search |
| Consistent CRUD verbs | get, list, create, update, delete | fetch, retrieve, make, add, remove |
Tool descriptions are equally important. The AI uses the description field to decide when to call the tool. A good description answers three questions: what does this do, when should you use it, and what data does it return?
TypeScript — well-annotated tool registrationserver.tool(
'github_search_repositories',
{
query: z.string().describe(
'Search query using GitHub search syntax. Supports qualifiers like ' +
'language:typescript, stars:>100, user:octocat. Example: "mcp server language:typescript"'
),
limit: z.number().int().min(1).max(30).default(10)
.describe('Maximum number of repositories to return (1-30, default 10)')
},
async ({ query, limit }) => { /* ... */ },
{
description:
'Search GitHub repositories by keyword, language, stars, or owner. ' +
'Returns repo name, description, star count, URL, and primary language. ' +
'Use when the user asks to find GitHub repos, explore open-source projects, ' +
'or check if a specific project exists on GitHub.'
}
);
The env block in claude_desktop_config.json is the primary way to pass secrets to stdio servers. But hardcoding tokens in a JSON file that might be committed to version control is a serious security risk. Here are the patterns to avoid credential leakage.
Instead of putting the secret directly in the config, call a shell script that reads from the OS keychain:
~/.mcp-wrappers/github.sh#!/bin/bash # Fetch token from macOS Keychain at runtime export GITHUB_PERSONAL_ACCESS_TOKEN=$(security find-generic-password \ -s "github-mcp-token" -w 2>/dev/null) exec npx -y @modelcontextprotocol/server-github "$@"
claude_desktop_config.json — wrapper pattern"github": {
"command": "/bin/bash",
"args": ["/Users/you/.mcp-wrappers/github.sh"]
}
For servers you build yourself, load secrets from a .env file in the server's directory using dotenv:
src/index.ts — dotenv loadingimport { config } from 'dotenv'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); config({ path: resolve(__dirname, '../.env') }); // Now process.env.MY_SECRET is available const apiKey = process.env.MY_API_KEY; if (!apiKey) { console.error('[my-server] MY_API_KEY not set — exiting'); process.exit(1); }
Use Zod to validate all required env vars at startup, providing clear error messages when something is missing:
src/config.ts — fail-fast env validationimport { z } from 'zod';
const EnvSchema = z.object({
GITHUB_TOKEN: z.string().min(1, 'GITHUB_TOKEN is required'),
SLACK_BOT_TOKEN: z.string().startsWith('xoxb-', 'Must be a bot token'),
DATABASE_URL: z.string().url('DATABASE_URL must be a valid URL'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
PORT: z.coerce.number().int().min(1024).max(65535).default(3000)
});
const result = EnvSchema.safeParse(process.env);
if (!result.success) {
console.error('[config] Invalid environment:');
result.error.issues.forEach(i => console.error(` - ${i.path.join('.')}: ${i.message}`));
process.exit(1);
}
export const config = result.data;
claude_desktop_config.json to git if it contains real tokens. Add it to .gitignore. If you need to share your config structure with a team, create a claude_desktop_config.example.json with placeholder values ("GITHUB_TOKEN": "YOUR_TOKEN_HERE") and commit that instead.When Claude Desktop spawns a stdio process, the child process receives only the variables in the env object — it does not inherit the parent's full environment. This means system variables like PATH, HOME, and NODE_ENV must be explicitly passed if the server needs them:
passing system vars explicitly"my-server": {
"command": "node",
"args": ["/path/to/dist/index.js"],
"env": {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"HOME": "/Users/you",
"NODE_ENV": "production",
"MY_API_KEY": "secret"
}
}
Static tool registration — calling server.tool() before server.connect() — covers most use cases. But some servers need to change their tool list dynamically: multi-tenant servers where tools depend on user permissions, plugin-style architectures where tools are loaded from a database, or feature flags that toggle tools on/off.
MCP supports dynamic tool updates via the tools/list_changed notification. When a server changes its tool list, it notifies connected clients, which then re-fetch the list with tools/list.
dynamic-tools-server.ts — adding/removing tools at runtimeimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({
name: 'dynamic-tools',
version: '1.0.0'
});
// Initial tools — always available
server.tool('ping', {}, async () => ({
content: [{ type: 'text', text: 'pong' }]
}));
// ── Feature-flag controlled tool ──────────────────────────────
let premiumEnabled = false;
server.tool(
'admin_enable_premium',
{ enabled: z.boolean() },
async ({ enabled }) => {
premiumEnabled = enabled;
if (enabled) {
// Register premium tool dynamically
server.tool(
'premium_deep_analysis',
{ data: z.string() },
async ({ data }) => ({
content: [{ type: 'text', text: `Deep analysis of: ${data}` }]
})
);
} else {
// Remove premium tool — not yet directly supported,
// but you can rebuild the server's tool set by restarting
// or use setRequestHandler at the lower level
}
// Notify clients that tool list changed
await server.server.sendToolListChanged();
return {
content: [{ type: 'text', text: `Premium features ${enabled ? 'enabled' : 'disabled'}` }]
};
}
);
// ── Permission-based tool loading ─────────────────────────────
async function loadToolsForUser(userId: string) {
const permissions = await fetchUserPermissions(userId); // your DB call
if (permissions.includes('write:database')) {
server.tool(
'db_execute_statement',
{ sql: z.string() },
async ({ sql }) => {
// ... execute with user context
return { content: [{ type: 'text', text: 'Executed' }] };
}
);
}
await server.server.sendToolListChanged();
}
const transport = new StdioServerTransport();
await server.connect(transport);
As of early 2026, there is no official Anthropic-run MCP registry. Instead, a healthy ecosystem of community registries has emerged. Understanding them helps you both discover existing servers and get your own server in front of users.
keywords:mcp-server to find all published MCP server packages. Direct install via npx. Thousands of packages already indexed.The MCP specification hints at a future /.well-known/mcp-registry.json endpoint and a central registry API. Based on the OAuth Dynamic Client Registration pattern (which MCP already uses), a registry entry would likely look something like this:
hypothetical registry entry format{
"name": "@yourscope/mcp-server-weather",
"displayName": "Weather MCP Server",
"version": "1.2.0",
"description": "Real-time weather data via OpenWeatherMap",
"author": "Your Name",
"transport": ["stdio"],
"runtime": "node",
"installCommand": "npx -y @yourscope/mcp-server-weather",
"envRequired": [
{
"name": "OPENWEATHER_API_KEY",
"description": "API key from openweathermap.org",
"link": "https://openweathermap.org/api"
}
],
"tools": [
{ "name": "get_weather", "description": "Get current weather for a location" },
{ "name": "get_forecast", "description": "Get 7-day forecast" }
],
"verified": true,
"securityAudit": "2026-01-15"
}
A discoverable server is one that hosts and users can understand at a glance — just by reading its metadata. Good metadata also improves the AI's tool selection accuracy, because the LLM uses tool names and descriptions as signals during reasoning.
Here is a complete, well-annotated server bootstrap that follows all best practices:
src/index.ts — self-describing server template#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { config } from './config.js'; // Zod-validated env (see Section 7)
// ── Server identity ──────────────────────────────────────────
const server = new McpServer({
name: 'weather', // short, kebab-case, unique across ecosystem
version: '1.0.0' // semver — bump on every breaking tool change
});
// ── Resource: server health ──────────────────────────────────
server.resource(
'server://health',
'mcp-weather://health',
{ mimeType: 'application/json' },
async () => ({
contents: [{
uri: 'mcp-weather://health',
mimeType: 'application/json',
text: JSON.stringify({
status: 'ok',
version: '1.0.0',
uptime: process.uptime(),
tools: ['get_weather', 'get_forecast', 'list_cities'],
dataSource: 'OpenWeatherMap API v2.5'
}, null, 2)
}]
})
);
// ── Tool: get current weather ────────────────────────────────
server.tool(
'get_weather',
{
location: z.string()
.min(2)
.max(100)
.describe('City name (e.g. "London"), "lat,lon" pair, or zip code with country (e.g. "10001,US")')
},
async ({ location }) => {
const url = new URL('https://api.openweathermap.org/data/2.5/weather');
url.searchParams.set('q', location);
url.searchParams.set('appid', config.OPENWEATHER_API_KEY);
url.searchParams.set('units', 'metric');
const res = await fetch(url);
if (!res.ok) {
return {
content: [{ type: 'text', text: `Error: ${res.status} — city not found or API error` }],
isError: true
};
}
const d = await res.json() as any;
return {
content: [{
type: 'text',
text: [
`📍 ${d.name}, ${d.sys.country}`,
`🌡️ Temperature: ${d.main.temp}°C (feels like ${d.main.feels_like}°C)`,
`☁️ Conditions: ${d.weather[0].description}`,
`💧 Humidity: ${d.main.humidity}%`,
`🌬️ Wind: ${d.wind.speed} m/s`
].join('\n')
}]
};
},
{
description:
'Get current weather conditions for any city or location. ' +
'Returns temperature, conditions, humidity, and wind speed. ' +
'Use for real-time weather queries. ' +
'Note: data updates every 10 minutes from OpenWeatherMap.'
}
);
// ── Startup ──────────────────────────────────────────────────
process.on('SIGTERM', async () => {
console.error('[weather] shutting down gracefully');
process.exit(0);
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[weather] server started — waiting for connections');
server://health resource is a community convention — not in the spec, but widely adopted. It lets operators quickly verify the server is running and see its capabilities without reading source code. Monitoring tools can poll it to check server status.A production Claude Desktop setup might run 5–10 MCP servers simultaneously. When tools overlap, when two servers return contradictory data, or when a request could go to multiple servers — the host needs a clear resolution strategy.
Claude Desktop presents all tools from all active servers to the model in a flat list. The model uses tool names and descriptions to pick the right one. This is why good naming (Section 6) is critical: if two servers have a tool called search, Claude will pick one seemingly at random.
When building your own host application (a custom MCP client), you have more control. Here is a multi-server registry with explicit tool routing:
multi-server-registry.ts — explicit routingimport { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
interface ServerEntry {
name: string;
client: Client;
tools: Set<string>;
priority: number; // higher = preferred when tools overlap
}
class MultiServerRegistry {
private servers: ServerEntry[] = [];
private toolIndex = new Map<string, ServerEntry>(); // toolName → server
async addServer(name: string, command: string, args: string[], priority = 0) {
const transport = new StdioClientTransport({ command, args });
const client = new Client({ name: 'orchestrator', version: '1.0.0' });
await client.connect(transport);
const { tools } = await client.listTools();
const toolNames = new Set(tools.map(t => t.name));
const entry: ServerEntry = { name, client, tools: toolNames, priority };
// Register tools, respecting priority on conflict
for (const toolName of toolNames) {
const existing = this.toolIndex.get(toolName);
if (!existing || priority > existing.priority) {
if (existing) {
console.warn(`[registry] Tool "${toolName}" overridden: ${existing.name} → ${name}`);
}
this.toolIndex.set(toolName, entry);
}
}
this.servers.push(entry);
console.log(`[registry] Registered ${name} with ${toolNames.size} tools`);
}
async callTool(toolName: string, args: Record<string, unknown>) {
const server = this.toolIndex.get(toolName);
if (!server) throw new Error(`Unknown tool: ${toolName}`);
return server.client.callTool({ name: toolName, arguments: args });
}
listAllTools() {
return [...this.toolIndex.entries()].map(([name, server]) => ({
name,
server: server.name
}));
}
async shutdown() {
await Promise.all(this.servers.map(s => s.client.close()));
}
}
// Usage
const registry = new MultiServerRegistry();
await registry.addServer('github', 'npx', ['-y', '@mcp/server-github'], 10);
await registry.addServer('gitlab', 'npx', ['-y', 'mcp-server-gitlab'], 5);
// github wins on any overlapping tools (priority 10 > 5)
const result = await registry.callTool('github_search_repos', { query: 'mcp server' });
Claude Code (the CLI) supports per-project configuration via .claude/settings.json in the project root. This means a Python project can have different servers than a TypeScript project:
.claude/settings.json — project-scoped MCP config{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"POSTGRES_CONNECTION_STRING": "postgresql://localhost/myapp_dev"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "./src"],
"env": {}
}
}
}
Before publishing or sharing your MCP server, run through this checklist. A discoverable, well-behaved server increases adoption, reduces support issues, and works correctly across different host applications.
servicename_action_noun. No generic names like search or create.
.describe()
Every parameter has a .describe() call explaining the expected format and valid values.
process.exit(1) with a clear error, not a silent runtime failure.
isError: true
Tool failures return { content: [...], isError: true } rather than throwing exceptions.
process.on('SIGTERM', ...) to close connections gracefully.
console.log → console.error. stdout is reserved for MCP JSON-RPC; any noise there breaks the protocol.
bin field
Entry point has #!/usr/bin/env node and package.json has "bin" pointing to it.
server://health resource returns version, uptime, and tool list as JSON.
claude_desktop_config.json block users need to copy-paste.
Beyond the checklist, consider these publishing steps:
npm run build && node dist/index.js and verify it starts without errors. Test with the MCP Inspector: npx @modelcontextprotocol/inspector dist/index.js.mcp-server keywordpackage.json, add "keywords": ["mcp", "mcp-server", "your-domain"]. This makes your package discoverable via npm search.npm login && npm publish --access public. Scoped packages (@yourname/mcp-server-x) require --access public on first publish.punkpeye/awesome-mcp-servers and submit to mcp.so via their submission form. Include your npm install command and env requirements."@modelcontextprotocol/sdk": "^1.0.0".readOnlyHint, destructiveHint, idempotentHint), pagination patterns for large result sets, and tool result content types (text, image, embedded resources).claude_desktop_config.json is used for remote Streamable HTTP servers?-y flag in "args": ["-y", "@scope/mcp-server-x"] when using npx?