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.
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
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(),
}],
};
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...
# 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:
- Overview: What does it do?
- Features: List of capabilities
- Installation: How to install
- Configuration: Required settings
- Usage: Examples of commands/tools
- Development: How to contribute
- 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
// 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,
};
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
// 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
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