Skip to content

feat(qoder): add Qoder MCP and commands support (2/4)#1601

Open
zxhdaniel wants to merge 2 commits into
dyoshikawa:mainfrom
zxhdaniel:feat/qoder-mcp-commands
Open

feat(qoder): add Qoder MCP and commands support (2/4)#1601
zxhdaniel wants to merge 2 commits into
dyoshikawa:mainfrom
zxhdaniel:feat/qoder-mcp-commands

Conversation

@zxhdaniel
Copy link
Copy Markdown

Summary

Add MCP and commands support for the Qoder tool target — the second PR in a series of 4 for full Qoder integration.

About Qoder: Qoder is an AI coding tool developed by Alibaba Group. Documentation: https://docs.qoder.com

Changes

  • Implement QoderMcp adapter for .qoder/mcp.json
  • Implement QoderCommand adapter for .qoder/commands/*.md
  • Register both in their respective processors
  • Add gitignore entries for .qoder/mcp.json and .qoder/commands/

PR Series

This is PR 2 of 4 for full Qoder support:

  1. Rules (feat(qoder): add Qoder rules support (1/4) #1600)
  2. MCP + Commands (this PR)
  3. Subagents + Skills
  4. Hooks

Note: This PR depends on #1600 (adds "qoder" to ALL_TOOL_TARGETS).

Test plan

  • All existing tests pass
  • Type checking passes
  • Format check passes (oxfmt --check .)

Made with Cursor

zxhdaniel added 2 commits May 6, 2026 09:37
Add Qoder (https://qoder.com) as a new tool target with rules support.

Qoder is an AI coding tool developed by Alibaba Group, designed for
enterprise-grade intelligent code generation and development assistance.

- Add "qoder" to ALL_TOOL_TARGETS
- Add qoder-specific fields to RulesyncRuleFrontmatterSchema
- Implement QoderRule adapter for .qoder/rules/*.md with YAML frontmatter
- Smart trigger inference maps rulesync canonical fields to Qoder's four
  native trigger modes: always_on, glob, model_decision, manual
- Register QoderRule in rules-processor
- Add .qoder/rules/ gitignore entry

Qoder documentation: https://docs.qoder.com/user-guide/rules

Co-authored-by: Cursor <cursoragent@cursor.com>

AI-Contributed/Feature: 0/285
AI-Contributed/UT: 0/501
Add MCP and commands support for the Qoder tool target.

- Implement QoderMcp adapter for .qoder/mcp.json
- Implement QoderCommand adapter for .qoder/commands/*.md
- Register both in their respective processors
- Add gitignore entries for .qoder/mcp.json and .qoder/commands/

Co-authored-by: Cursor <cursoragent@cursor.com>

AI-Contributed/Feature: 0/304
AI-Contributed/UT: 0/457
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

Thank you for your contribution! Unfortunately, this PR has 1547 added lines, which exceeds the limit of 1000 lines for external contributors.

Please split your changes into smaller PRs. See CONTRIBUTING.md for details.

@dyoshikawa
Copy link
Copy Markdown
Owner

@zxhdaniel Please work on the ci failure.

Copy link
Copy Markdown
Collaborator

@dyoshikawa-claw dyoshikawa-claw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is a solid implementation — it follows the existing patterns in the codebase consistently, the tests are thorough, and the trigger inference logic in QoderRule.fromRulesyncRule is well thought out.

The CI failure in oxlint is the main blocker to address (the != usage in qoder-rule.ts). A couple of other minor things: the command adapter uses outputRoot: "." where process.cwd() would be more consistent with other adapters, and the MCP adapter's fromRulesyncMcp fully overwrites the target file — consider merging with existing content like CursorMcp does to preserve user-added MCP servers.

const description = fm.qoder?.description ?? fm.description;

const isCatchAll =
globs != null && globs.length > 0 && globs.every((g) => g === "**/*" || g === "**");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use !== instead of != here — this is what's causing the oxlint CI failure. TypeScript's strict null checks make != unnecessary, and the linter enforces !==.


const isCatchAll =
globs != null && globs.length > 0 && globs.every((g) => g === "**/*" || g === "**");
const hasSpecificGlobs = globs != null && globs.length > 0 && !isCatchAll;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same !=!== issue as above.

const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);

return new RulesyncCommand({
outputRoot: ".",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be outputRoot: process.cwd() — using "." breaks when cwd differs from the mocked test directory. Other command adapters (e.g. junie-command.ts, geminicli-command.ts) use process.cwd().


constructor(params: ToolMcpParams) {
super(params);
this.json = this.fileContent !== undefined ? JSON.parse(this.fileContent) : {};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON.parse can throw on malformed input — consider wrapping in try/catch with a descriptive error message that includes the file path, like CursorMcp does. Helps users debug their .qoder/mcp.json.

});
}

static fromRulesyncMcp({
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fromRulesyncMcp replaces the entire file content without reading the existing .qoder/mcp.json first. If users have manually added MCP servers outside of rulesync, those get lost on generate. CursorMcp.fromRulesyncMcp shows the pattern for reading the existing file and merging. If this overwrite is intentional, a comment would help future readers.

if (alwaysApply === true || isCatchAll) {
qoderFrontmatter = { trigger: "always_on", alwaysApply: true };
} else if (hasSpecificGlobs) {
qoderFrontmatter = { trigger: "glob", glob: globs!.join(",") };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-null assertion globs! here is safe due to the hasSpecificGlobs guard above, but TypeScript can't narrow through that variable. Consider extracting globs to a local const after a null check to avoid the assertion entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants