Skip to main content

Extension Best Practices

Follow these guidelines to create high-quality, maintainable, and user-friendly extensions.

Extension Structure

Keep It Organized

Use a clear, logical directory structure:
my-extension/
├── qwen-extension.json      # Required manifest
├── README.md               # User documentation
├── LICENSE                 # License file
├── CHANGELOG.md            # Version history
├── .gitignore              # Git ignore rules
├── QWEN.md                 # Context (if needed)
├── commands/               # Custom commands
│   ├── category1/
│   │   └── command1.md
│   └── category2/
│       └── command2.md
├── skills/                 # Skills
│   └── skill-name/
│       └── SKILL.md
├── agents/                 # Subagents
│   ├── agent1.md
│   └── agent2.md
└── mcp-server/            # MCP server code (if applicable)
    ├── src/
    │   └── server.ts
    ├── dist/
    │   └── server.js
    ├── package.json
    └── tsconfig.json

Name Things Clearly

Extension names:
// Good
"name": "github-tools"
"name": "database-query"
"name": "code-reviewer"

// Bad
"name": "myExtension"         // Not kebab-case
"name": "tools_for_stuff"     // Underscores instead of dashes
"name": "ext"                 // Too vague
Command files:
✅ commands/analyze-dependencies.md
✅ commands/test/run-e2e.md
❌ commands/cmd1.md
❌ commands/DoStuff.md
Skill/Agent names:
# Good
name: code-reviewer
name: test-generator
name: api-designer

# Bad
name: Helper
name: tool_1
name: myAgent

Manifest File (qwen-extension.json)

Complete and Accurate

{
  "name": "my-extension",
  "version": "1.0.0",
  "mcpServers": {
    "myServer": {
      "command": "node",
      "args": ["${extensionPath}${/}dist${/}server.js"],
      "cwd": "${extensionPath}",
      "description": "Provides tools for X, Y, and Z"
    }
  },
  "contextFileName": "QWEN.md",
  "commands": "commands",
  "skills": "skills",
  "agents": "agents",
  "settings": [
    {
      "name": "API Key",
      "description": "Your API key from https://example.com/api-keys",
      "envVar": "MY_EXTENSION_API_KEY",
      "sensitive": true
    }
  ]
}

Use Variables Properly

// Good - portable across platforms
"args": ["${extensionPath}${/}dist${/}server.js"]

// Bad - hardcoded paths
"args": ["/Users/me/.qwen/extensions/my-ext/dist/server.js"]
"args": ["C:\\Users\\me\\.qwen\\extensions\\my-ext\\dist\\server.js"]

// Bad - wrong path separator
"args": ["${extensionPath}/dist/server.js"]  // Breaks on Windows

Semantic Versioning

Follow semver:
{
  "version": "1.0.0"   // MAJOR.MINOR.PATCH
}
  • MAJOR: Breaking changes
  • MINOR: New features (backwards compatible)
  • PATCH: Bug fixes

MCP Servers

Tool Design

Clear, descriptive names:
// Good
server.registerTool('search_repositories', ...)
server.registerTool('create_pull_request', ...)

// Bad
server.registerTool('search', ...)  // Too vague
server.registerTool('do_stuff', ...)
Comprehensive descriptions:
// Good
{
  description: 'Search GitHub repositories by query string, language, and stars. Returns repository name, description, URL, and star count.',
  inputSchema: z.object({
    query: z.string().describe('Search terms (supports GitHub search syntax)'),
    language: z.string().optional().describe('Filter by programming language'),
    min_stars: z.number().optional().describe('Minimum star count'),
  }).shape,
}

// Bad
{
  description: 'Searches repos',
  inputSchema: z.object({
    q: z.string(),
    lang: z.string().optional(),
  }).shape,
}
Validate input thoroughly:
inputSchema: z.object({
  email: z.string().email().describe('Valid email address'),
  age: z.number().int().min(0).max(150).describe('Age in years'),
  url: z.string().url().describe('Valid HTTP(S) URL'),
  items: z.array(z.string()).min(1).max(100).describe('1-100 items'),
}).shape,
Handle errors gracefully:
async ({ param }) => {
  try {
    const result = await externalApiCall(param);
    
    if (!result) {
      return {
        content: [{
          type: 'text',
          text: 'No results found',
        }],
      };
    }
    
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(result, null, 2),
      }],
    };
  } catch (error) {
    return {
      content: [{
        type: 'text',
        text: `Error: ${error.message}\n\nPlease check your configuration and try again.`,
      }],
      isError: true,
    };
  }
}
Format output clearly:
// Good - structured JSON
return {
  content: [{
    type: 'text',
    text: JSON.stringify({
      status: 'success',
      count: results.length,
      results: results.map(r => ({
        id: r.id,
        name: r.name,
        description: r.description,
      })),
    }, null, 2),
  }],
};

// Bad - unformatted dump
return {
  content: [{
    type: 'text',
    text: results.toString(),
  }],
};

Performance

Add timeouts:
const timeout = (ms: number) => new Promise((_, reject) => 
  setTimeout(() => reject(new Error('Timeout')), ms)
);

const result = await Promise.race([
  externalApiCall(params),
  timeout(30000),  // 30 second timeout
]);
Cache when appropriate:
const cache = new Map<string, { data: any; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000;  // 5 minutes

function getCached(key: string) {
  const cached = cache.get(key);
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return cached.data;
  }
  return null;
}

server.registerTool('get_data', { ... }, async ({ query }) => {
  const cacheKey = `data:${query}`;
  const cached = getCached(cacheKey);
  
  if (cached) {
    return cached;
  }
  
  const data = await fetchData(query);
  cache.set(cacheKey, { data, timestamp: Date.now() });
  
  return data;
});
Limit response sizes:
const MAX_RESULTS = 100;

server.registerTool('search', { ... }, async ({ query, limit = 10 }) => {
  const safeLimit = Math.min(limit, MAX_RESULTS);
  const results = await search(query, safeLimit);
  
  return {
    content: [{
      type: 'text',
      text: JSON.stringify({
        count: results.length,
        total: results.total,
        results: results.slice(0, safeLimit),
      }, null, 2),
    }],
  };
});

Commands

Clear and Focused

# Good - Single purpose
---
description: Search for TODO comments and list them by file
---

TODO comments in the project:

!{grep -r "TODO" . --include="*.ts" --include="*.js"}

Please organize these by file and prioritize them.

# Bad - Does too much
---
description: Search and fix TODOs and also analyze code quality
---
...

Helpful Descriptions

# Good
---
description: Analyzes package.json dependencies and suggests updates, including security advisories
---

# Bad
---
description: Checks dependencies
---

Safe Shell Commands

# Good - Limited output
!{git log --oneline -20}
!{ls -la | head -50}
!{grep -r "pattern" . | head -100}

# Bad - Unlimited output (could be huge)
!{git log}
!{find . -type f}
!{cat huge-file.log}

Error Handling

# Good - Handles failures
!{cat "{{args}}" 2>/dev/null || echo "File not found: {{args}}"}

# Bad - No error handling
!{cat "{{args}}"}

Skills

Descriptive Frontmatter

# Good
---
name: performance-analyzer
description: Analyzes code performance, identifies bottlenecks, calculates complexity, and suggests optimizations. Use when user asks about performance, speed, or optimization.
---

# Bad
---
name: perf
description: Performance stuff
---

Structured Instructions

# Code Review Skill

## When to Use

[Clear trigger conditions]

## Review Process

1. **Step 1**: [What to do]
2. **Step 2**: [What to do]
3. **Step 3**: [What to do]

## Output Format

[Structured format]

## Best Practices

[Guidelines to follow]

Include Examples

## Example

**Input**: Function that validates email addresses

**Analysis**:
- Check regex pattern correctness
- Test edge cases
- Verify error handling

**Output**: Review with specific suggestions

Agents

Focused Expertise

# Good - Specific role
---
name: api-designer
description: Specialized in designing RESTful APIs
tools:
  - Read
  - Write
---

You are an API design specialist...

# Bad - Too broad
---
name: coding-helper
description: Helps with coding
tools:
  - Read
  - Write
  - Bash
  - Grep
  - WebFetch
---

You help with code...

Appropriate Tools

# Good - Only necessary tools
tools:
  - Read      # To read code
  - Write     # To write docs
  - Grep      # To search

# Bad - Excessive tools
tools:
  - Read
  - Write
  - Edit
  - Grep
  - Glob
  - Bash
  - WebFetch
  - WebSearch
  - TodoWrite

Clear Instructions

You are a test writing specialist.

## Your Approach

1. **Understand**: Read and analyze the code
2. **Plan**: Identify test cases needed
3. **Write**: Create clear, focused tests
4. **Verify**: Ensure tests run and pass

## Testing Principles

- Test behavior, not implementation
- Use descriptive test names
- Follow AAA pattern (Arrange, Act, Assert)
- Mock external dependencies
- Cover edge cases and errors

Context Files (QWEN.md)

Be Concise

Context consumes tokens. Be comprehensive but brief:
# Good
### query_database
Executes SQL queries (read-only). Always use LIMIT.
Example: SELECT * FROM users LIMIT 100

# Bad (too verbose)
### query_database
This tool allows you to execute SQL queries against the configured database.
It's important to note that this is a read-only connection, so you cannot
perform INSERT, UPDATE, or DELETE operations. When you write queries, you
should always include a LIMIT clause to avoid returning too many rows...
[continues for several paragraphs]

Document Constraints

## Important Constraints

- Query timeout: 30 seconds
- Max results: 1000 rows
- Read-only access
- Requires API key configured

Provide Examples

## Example Usage

1. List tables: `list_tables()`
2. Get schema: `describe_table({table: "users"})`
3. Query: `query_database({query: "SELECT * FROM users LIMIT 10"})`

Settings and Configuration

Clear Setting Descriptions

// Good
{
  "name": "GitHub Personal Access Token",
  "description": "Create at https://github.com/settings/tokens with 'repo' scope",
  "envVar": "GITHUB_TOKEN",
  "sensitive": true
}

// Bad
{
  "name": "Token",
  "description": "Your token",
  "envVar": "TOKEN",
  "sensitive": true
}

Sensitive Data Handling

{
  "settings": [
    {
      "name": "API Key",
      "envVar": "API_KEY",
      "sensitive": true     // Stored in keychain
    },
    {
      "name": "API Endpoint",
      "envVar": "API_URL",
      "sensitive": false    // Stored in .env file
    }
  ]
}

Provide Defaults

// In MCP server
const API_URL = process.env.API_URL || 'https://api.example.com';
const TIMEOUT = parseInt(process.env.TIMEOUT || '30000');
const MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '3');

Documentation

Comprehensive README

Include:
  1. Overview: What does it do?
  2. Features: List of capabilities
  3. Installation: How to install
  4. Configuration: Required settings
  5. Usage: Examples of commands/tools
  6. Development: How to contribute
  7. License: License information

Keep CHANGELOG

# Changelog

All notable changes to this extension will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/).

## [1.2.0] - 2024-01-20

### Added
- New `analyze` command for code analysis
- Support for TypeScript type checking

### Fixed
- Issue with timeout on large files

### Changed
- Improved error messages

## [1.1.0] - 2024-01-10

### Added
- Multi-file support for commands

## [1.0.0] - 2024-01-01

- Initial release

Testing

Test Before Release

# Link locally
qwen extensions link .

# Test all features
# - Try all commands
# - Test all MCP tools
# - Verify agents work
# - Check skills are discovered

# Test with fresh install
qwen extensions uninstall my-extension
qwen extensions install /path/to/my-extension

Test Edge Cases

  • Empty inputs
  • Invalid inputs
  • Missing configuration
  • Network failures (for API-based tools)
  • Large inputs/outputs
  • Special characters in inputs

Security

Validate User Input

// Good
server.registerTool('read_file', { ... }, async ({ path }) => {
  // Validate path
  if (path.includes('..') || path.startsWith('/')) {
    return {
      content: [{ type: 'text', text: 'Invalid path' }],
      isError: true,
    };
  }
  
  const safePath = join(process.cwd(), path);
  // ... read file
});

// Bad - No validation
server.registerTool('read_file', { ... }, async ({ path }) => {
  return fs.readFileSync(path, 'utf-8');  // Dangerous!
});

Protect Sensitive Data

// Good - Never log sensitive data
if (DEBUG) {
  console.error('Making API call to:', endpoint);
  // DON'T log: API_KEY, passwords, tokens
}

// Bad
console.log('Using API key:', API_KEY);

Use HTTPS

// Good
const API_URL = 'https://api.example.com';

// Bad
const API_URL = 'http://api.example.com';

Error Messages

Be Helpful

// Good
throw new Error(
  'GitHub token not configured. '
  + 'Set it with: qwen extensions settings set github-tools "GitHub Token"'
);

// Bad
throw new Error('No token');

Provide Context

// Good
return {
  content: [{
    type: 'text',
    text: `Failed to fetch repositories for "${owner}":\n`
      + `Error: ${error.message}\n\n`
      + `Please check that:\n`
      + `1. The username "${owner}" exists\n`
      + `2. Your GitHub token has correct permissions\n`
      + `3. You have network connectivity`,
  }],
  isError: true,
};

// Bad
return {
  content: [{ type: 'text', text: 'Error' }],
  isError: true,
};

Performance

Optimize Startup

  • Don’t do heavy initialization in MCP server startup
  • Lazy-load large dependencies
  • Cache expensive computations

Minimize Context

  • Keep QWEN.md concise
  • Don’t include unnecessary information
  • Use external docs for detailed information

Efficient Tools

// Good - Stream large responses
server.registerTool('fetch_large_data', { ... }, async ({ id }) => {
  const stream = await fetchDataStream(id);
  const chunks = [];
  
  for await (const chunk of stream) {
    chunks.push(chunk);
    if (chunks.length > 1000) break;  // Limit
  }
  
  return { content: [{ type: 'text', text: chunks.join('') }] };
});

// Bad - Load everything into memory
server.registerTool('fetch_large_data', { ... }, async ({ id }) => {
  const allData = await fetchAllData(id);  // Could be gigabytes
  return { content: [{ type: 'text', text: allData }] };
});

Compatibility

Cross-Platform

Use path separators correctly:
// Good
"args": ["${extensionPath}${/}dist${/}server.js"]

// Bad
"args": ["${extensionPath}/dist/server.js"]

Node Version

Support LTS Node versions:
{
  "engines": {
    "node": ">=20.0.0"
  }
}

Next Steps