> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/QwenLM/qwen-code/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Server Extensions

> Create Model Context Protocol servers to add custom tools through extensions

# Creating MCP Server Extensions

Model Context Protocol (MCP) servers allow you to add custom tools that the AI can use. Through extensions, you can package and distribute MCP servers with all necessary configuration, making it easy for others to install and use your tools.

## What are MCP Servers?

MCP servers are:

* Standalone processes that communicate via stdio (or other transports)
* Provide tools (functions) the AI can call
* Can expose prompts and resources
* Run continuously during your session
* Defined using the `@modelcontextprotocol/sdk`

## When to Use MCP Servers

Use MCP servers when you need to:

* **Integrate External APIs**: GitHub, Slack, databases, etc.
* **Add Specialized Tools**: Image processing, data analysis, etc.
* **Access Resources**: Files, databases, web services
* **Provide Prompts**: Pre-configured AI prompts
* **Execute Operations**: Deploy, test, monitor, etc.

## Extension Structure

```
my-mcp-extension/
├── qwen-extension.json
├── package.json
├── tsconfig.json
├── server.ts              # MCP server source
└── dist/
    └── server.js          # Built server
```

## Quick Start: Using Template

Create from the built-in MCP server template:

```bash theme={null}
qwen extensions new my-mcp-extension mcp-server
cd my-mcp-extension
npm install
npm run build
qwen extensions link .
```

## Manual Setup

### Step 1: Create Package Files

**package.json:**

```json theme={null}
{
  "name": "my-mcp-extension",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.11.0",
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "@types/node": "^20.11.25",
    "typescript": "~5.4.5"
  }
}
```

**tsconfig.json:**

```json theme={null}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["server.ts"]
}
```

### Step 2: Create MCP Server

**server.ts:**

```typescript theme={null}
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: 'my-mcp-extension',
  version: '1.0.0',
});

// Register tools here (see examples below)

const transport = new StdioServerTransport();
await server.connect(transport);
```

### Step 3: Configure Extension

**qwen-extension.json:**

```json theme={null}
{
  "name": "my-mcp-extension",
  "version": "1.0.0",
  "mcpServers": {
    "myServer": {
      "command": "node",
      "args": ["${extensionPath}${/}dist${/}server.js"],
      "cwd": "${extensionPath}"
    }
  }
}
```

### Step 4: Build and Test

```bash theme={null}
npm install
npm run build
qwen extensions link .
```

Restart Qwen Code to load the extension.

## Registering Tools

### Simple Tool

```typescript theme={null}
server.registerTool(
  'get_greeting',
  {
    description: 'Returns a greeting message',
    inputSchema: z.object({
      name: z.string().describe('Name to greet'),
    }).shape,
  },
  async ({ name }) => {
    return {
      content: [
        {
          type: 'text',
          text: `Hello, ${name}!`,
        },
      ],
    };
  },
);
```

### Tool with Multiple Parameters

```typescript theme={null}
server.registerTool(
  'search_users',
  {
    description: 'Search for users by various criteria',
    inputSchema: z.object({
      query: z.string().describe('Search query'),
      status: z.enum(['active', 'inactive', 'all'])
        .optional()
        .describe('Filter by status'),
      limit: z.number()
        .min(1)
        .max(100)
        .optional()
        .describe('Maximum results to return'),
    }).shape,
  },
  async ({ query, status = 'all', limit = 10 }) => {
    // Implement search logic
    const results = await searchUsers(query, status, limit);
    
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  },
);
```

### Tool with API Call

```typescript theme={null}
server.registerTool(
  'fetch_posts',
  {
    description: 'Fetches posts from a public API',
    inputSchema: z.object({}).shape,
  },
  async () => {
    const response = await fetch(
      'https://jsonplaceholder.typicode.com/posts'
    );
    const posts = await response.json();
    
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(posts.slice(0, 5), null, 2),
        },
      ],
    };
  },
);
```

### Tool with Error Handling

```typescript theme={null}
server.registerTool(
  'divide_numbers',
  {
    description: 'Divides two numbers',
    inputSchema: z.object({
      dividend: z.number().describe('Number to divide'),
      divisor: z.number().describe('Number to divide by'),
    }).shape,
  },
  async ({ dividend, divisor }) => {
    if (divisor === 0) {
      return {
        content: [
          {
            type: 'text',
            text: 'Error: Cannot divide by zero',
          },
        ],
        isError: true,
      };
    }
    
    const result = dividend / divisor;
    return {
      content: [
        {
          type: 'text',
          text: `Result: ${result}`,
        },
      ],
    };
  },
);
```

## Registering Prompts

Prompts are pre-configured AI prompts that users can invoke:

```typescript theme={null}
server.registerPrompt(
  'code-reviewer',
  {
    title: 'Code Review',
    description: 'Reviews code for quality and issues',
    argsSchema: {
      code: z.string().describe('Code to review'),
      language: z.string().optional().describe('Programming language'),
    },
  },
  ({ code, language }) => ({
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: `Review this ${language || 'code'}:\n\n${code}\n\nProvide feedback on quality, potential issues, and improvements.`,
        },
      },
    ],
  }),
);
```

## Using Environment Variables

For API keys and sensitive data:

### Define Settings

**qwen-extension.json:**

```json theme={null}
{
  "name": "my-mcp-extension",
  "version": "1.0.0",
  "settings": [
    {
      "name": "API Key",
      "description": "Your API key for the service",
      "envVar": "MY_SERVICE_API_KEY",
      "sensitive": true
    },
    {
      "name": "Base URL",
      "description": "API base URL",
      "envVar": "MY_SERVICE_BASE_URL",
      "sensitive": false
    }
  ],
  "mcpServers": {
    "myServer": {
      "command": "node",
      "args": ["${extensionPath}${/}dist${/}server.js"],
      "cwd": "${extensionPath}",
      "env": {
        "API_KEY": "${MY_SERVICE_API_KEY}",
        "BASE_URL": "${MY_SERVICE_BASE_URL}"
      }
    }
  }
}
```

### Access in Server

```typescript theme={null}
const API_KEY = process.env.API_KEY;
const BASE_URL = process.env.BASE_URL || 'https://api.example.com';

server.registerTool(
  'api_call',
  {
    description: 'Calls the external API',
    inputSchema: z.object({
      endpoint: z.string(),
    }).shape,
  },
  async ({ endpoint }) => {
    const response = await fetch(`${BASE_URL}${endpoint}`, {
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
      },
    });
    
    const data = await response.json();
    return {
      content: [{ type: 'text', text: JSON.stringify(data) }],
    };
  },
);
```

## Example: GitHub MCP Server

A complete example integrating with GitHub API:

```typescript theme={null}
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { Octokit } from '@octokit/rest';

const server = new McpServer({
  name: 'github-tools',
  version: '1.0.0',
});

const octokit = new Octokit({
  auth: process.env.GITHUB_TOKEN,
});

server.registerTool(
  'list_repos',
  {
    description: 'List repositories for a user or organization',
    inputSchema: z.object({
      owner: z.string().describe('Username or organization'),
      type: z.enum(['all', 'owner', 'public', 'private'])
        .optional()
        .describe('Repository type'),
    }).shape,
  },
  async ({ owner, type = 'all' }) => {
    const { data } = await octokit.repos.listForUser({
      username: owner,
      type,
    });
    
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(
            data.map(repo => ({
              name: repo.name,
              description: repo.description,
              stars: repo.stargazers_count,
              language: repo.language,
            })),
            null,
            2
          ),
        },
      ],
    };
  },
);

server.registerTool(
  'create_issue',
  {
    description: 'Create a new issue in a repository',
    inputSchema: z.object({
      owner: z.string().describe('Repository owner'),
      repo: z.string().describe('Repository name'),
      title: z.string().describe('Issue title'),
      body: z.string().optional().describe('Issue description'),
      labels: z.array(z.string()).optional().describe('Labels to add'),
    }).shape,
  },
  async ({ owner, repo, title, body, labels }) => {
    const { data } = await octokit.issues.create({
      owner,
      repo,
      title,
      body,
      labels,
    });
    
    return {
      content: [
        {
          type: 'text',
          text: `Issue created: ${data.html_url}`,
        },
      ],
    };
  },
);

server.registerTool(
  'get_pr_status',
  {
    description: 'Get status of a pull request',
    inputSchema: z.object({
      owner: z.string(),
      repo: z.string(),
      pull_number: z.number(),
    }).shape,
  },
  async ({ owner, repo, pull_number }) => {
    const { data } = await octokit.pulls.get({
      owner,
      repo,
      pull_number,
    });
    
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify({
            title: data.title,
            state: data.state,
            mergeable: data.mergeable,
            merged: data.merged,
            comments: data.comments,
            commits: data.commits,
          }, null, 2),
        },
      ],
    };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);
```

**package.json** (add dependency):

```json theme={null}
{
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.11.0",
    "@octokit/rest": "^20.0.0",
    "zod": "^3.22.4"
  }
}
```

**qwen-extension.json:**

```json theme={null}
{
  "name": "github-tools",
  "version": "1.0.0",
  "settings": [
    {
      "name": "GitHub Token",
      "description": "Personal access token for GitHub API",
      "envVar": "GITHUB_TOKEN",
      "sensitive": true
    }
  ],
  "mcpServers": {
    "github": {
      "command": "node",
      "args": ["${extensionPath}${/}dist${/}server.js"],
      "cwd": "${extensionPath}",
      "env": {
        "GITHUB_TOKEN": "${GITHUB_TOKEN}"
      }
    }
  }
}
```

## Best Practices

### 1. Clear Tool Descriptions

```typescript theme={null}
// Good
description: 'Creates a new GitHub issue with title, body, and labels'

// Bad
description: 'Creates issue'
```

### 2. Validate Input

```typescript theme={null}
inputSchema: z.object({
  email: z.string().email().describe('Valid email address'),
  age: z.number().min(0).max(150).describe('Age in years'),
  status: z.enum(['active', 'inactive']).describe('Account status'),
}).shape,
```

### 3. Handle Errors Gracefully

```typescript theme={null}
async ({ param }) => {
  try {
    const result = await riskyOperation(param);
    return {
      content: [{ type: 'text', text: result }],
    };
  } catch (error) {
    return {
      content: [{
        type: 'text',
        text: `Error: ${error.message}`,
      }],
      isError: true,
    };
  }
}
```

### 4. Format Output Clearly

```typescript theme={null}
// Good - structured JSON
return {
  content: [{
    type: 'text',
    text: JSON.stringify({
      status: 'success',
      data: results,
      count: results.length,
    }, null, 2),
  }],
};

// Avoid - raw dumps
return {
  content: [{ type: 'text', text: results.toString() }],
};
```

### 5. Document Parameter Purpose

```typescript theme={null}
inputSchema: z.object({
  query: z.string()
    .describe('Search query - supports wildcards (*) and AND/OR operators'),
  limit: z.number()
    .min(1)
    .max(100)
    .optional()
    .describe('Max results (default: 10, max: 100)'),
}).shape,
```

## MCP Server Configuration

Advanced configuration options:

```json theme={null}
{
  "mcpServers": {
    "myServer": {
      "command": "node",
      "args": ["${extensionPath}${/}dist${/}server.js"],
      "cwd": "${extensionPath}",
      "env": {
        "NODE_ENV": "production",
        "LOG_LEVEL": "info"
      },
      "timeout": 30000,
      "description": "My custom MCP server",
      "includeTools": ["tool1", "tool2"],
      "excludeTools": ["admin_tool"]
    }
  }
}
```

## Debugging

Add logging to your MCP server:

```typescript theme={null}
const DEBUG = process.env.DEBUG === 'true';

function log(...args: any[]) {
  if (DEBUG) {
    console.error('[MCP Server]', ...args);
  }
}

server.registerTool('my_tool', { ... }, async (params) => {
  log('Tool called with params:', params);
  try {
    const result = await operation(params);
    log('Tool result:', result);
    return result;
  } catch (error) {
    log('Tool error:', error);
    throw error;
  }
});
```

Enable with:

```json theme={null}
"env": {
  "DEBUG": "true"
}
```

## Next Steps

* [Creating Extensions](/extensions/creating-extensions) - Complete extension guide
* [Context Providers](/extensions/context-providers) - Document your tools
* [Best Practices](/extensions/best-practices) - Development guidelines
* [Releasing](/extensions/releasing) - Publish your extension
