-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add qwen-omni-mcp bailian video/image server #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # Bailian (DashScope) API key — get from https://platform.qianwenai.com/home/api-keys | ||
| # REQUIRED. Treat as a secret; never commit this file. | ||
| DASHSCOPE_API_KEY= | ||
|
|
||
| # Model ID. qwen3.7-plus is multimodal (text+image+video) on Bailian. | ||
| QWEN_MODEL=qwen3.7-plus | ||
|
|
||
| # OpenAI-compatible endpoint (documented multimodal/video support). | ||
| # Anthropic-compatible /apps/anthropic does NOT support video input. | ||
| DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 | ||
|
|
||
| # Per-request timeout in seconds (video processing can be slow). | ||
| QWEN_REQUEST_TIMEOUT=300 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| ## Summary | ||
|
|
||
| <!-- What does this change do and why? --> | ||
|
|
||
| ## Checklist | ||
|
|
||
| - [ ] `npm run typecheck` passes | ||
| - [ ] `npm run lint` passes (`--max-warnings 0`) | ||
| - [ ] `npm run format:check` passes | ||
| - [ ] `npm test` passes | ||
| - [ ] No secrets, API keys, or `.env` files committed | ||
| - [ ] Did NOT use `git commit --no-verify` (see AGENTS.md) | ||
| - [ ] New tools/behavior covered by tests (unit + mocked e2e) | ||
| - [ ] Live behavior verified locally with `LIVE=1 npm run test:live` (if behavior changed) | ||
|
|
||
| ## Notes for reviewers | ||
|
|
||
| <!-- Anything non-obvious, fragile assumptions, or follow-ups --> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main, develop] | ||
| pull_request: | ||
|
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win No concurrency group defined. zizmor flags the workflow for missing a 🧰 Tools🪛 zizmor (1.26.1)[warning] 3-6: insufficient job-level concurrency limits (concurrency-limits): workflow is missing concurrency setting (concurrency-limits) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| quality: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| node: [20, 22] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: ${{ matrix.node }} | ||
| cache: npm | ||
|
Comment on lines
+19
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win Actions unpinned and zizmor flags Also applies to: 33-37 🧰 Tools🪛 zizmor (1.26.1)[warning] 19-19: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [error] 19-19: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) [error] 20-20: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| - name: Install (skip husky in CI) | ||
| run: npm ci | ||
| env: | ||
| HUSKY: "0" | ||
| - run: npm run typecheck | ||
| - run: npm run lint | ||
| - run: npm run format:check | ||
| - run: npm test | ||
| - run: npm run build | ||
| - uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: dist-node-${{ matrix.node }} | ||
| path: dist | ||
| if-no-files-found: error | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| name: Secret Scan | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main, develop] | ||
| pull_request: | ||
|
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win No concurrency group defined. Same finding as ci.yml — no 🧰 Tools🪛 zizmor (1.26.1)[warning] 3-6: insufficient job-level concurrency limits (concurrency-limits): workflow is missing concurrency setting (concurrency-limits) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| gitleaks: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| - uses: gitleaks/gitleaks-action@v2 | ||
|
Comment on lines
+15
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win Actions unpinned and Same as flagged in ci.yml: 🧰 Tools🪛 zizmor (1.26.1)[warning] 15-17: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [error] 15-15: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) [error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| env: | ||
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| name: Live Smoke | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| schedule: | ||
| # Weekly sanity check (Mon ~03:17 UTC, off the :00 mark). | ||
| - cron: "17 3 * * 1" | ||
|
Comment on lines
+3
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win No concurrency group defined. A manual 🧰 Tools🪛 zizmor (1.26.1)[warning] 3-7: insufficient job-level concurrency limits (concurrency-limits): workflow is missing concurrency setting (concurrency-limits) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| live: | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | ||
| LIVE: "1" | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-node@v4 | ||
|
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win Actions unpinned and Same finding as the other two workflows: 🧰 Tools🪛 zizmor (1.26.1)[warning] 19-19: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) [error] 19-19: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) [error] 20-20: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy) (unpinned-uses) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| with: | ||
| node-version: 20 | ||
| cache: npm | ||
| - name: Install (skip husky in CI) | ||
| run: npm ci | ||
| env: | ||
| HUSKY: "0" | ||
| - run: npm run build | ||
| - name: Run live image understanding (bundled fixture) | ||
| run: npm run test:live | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,39 @@ | ||
| # === Secrets — NEVER commit === | ||
| .env | ||
| .env.* | ||
| !.env.example | ||
|
|
||
| # === Node === | ||
| node_modules/ | ||
| dist/ | ||
| build/ | ||
| *.tsbuildinfo | ||
| *.log | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| pnpm-debug.log* | ||
| .npm | ||
| .pnpm-store/ | ||
|
|
||
| # === Test / coverage === | ||
| coverage/ | ||
| .nyc_output/ | ||
| .vitest-cache/ | ||
|
|
||
| # === Biome / tooling caches === | ||
| .biome-cache/ | ||
|
|
||
| # === IDE / editor === | ||
| .vscode/ | ||
| .idea/ | ||
| *.swp | ||
| *.swo | ||
| *~ | ||
|
|
||
| # === OS === | ||
| .DS_Store | ||
| Thumbs.db | ||
|
|
||
| # === Reference / vendor === | ||
| ref/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| #!/usr/bin/env sh | ||
| # Pre-commit: secret guard + lint/format staged files + gitleaks (if installed). | ||
| # NEVER bypass with --no-verify (see AGENTS.md). | ||
| node scripts/check-secrets.mjs || exit 1 | ||
| npx lint-staged || exit 1 | ||
| if command -v gitleaks >/dev/null 2>&1; then | ||
| gitleaks protect --staged --redact || exit 1 | ||
| else | ||
| echo "⚠ gitleaks not installed, skipping generic secret scan." | ||
| echo " Install: https://github.com/gitleaks/gitleaks" | ||
| fi | ||
|
Comment on lines
+6
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Make generic secret scanning fail closed. When
Based on learnings, “Never commit secrets, API keys, tokens, or 📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Learnings |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,5 @@ | ||||||||||||||||
| #!/usr/bin/env sh | ||||||||||||||||
| # Pre-push: type-check + full test suite (live tests auto-skip without LIVE=1). | ||||||||||||||||
| # NEVER bypass with --no-verify (see AGENTS.md). | ||||||||||||||||
| npm run typecheck || exit 1 | ||||||||||||||||
| npm test || exit 1 | ||||||||||||||||
|
Comment on lines
+4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Run every required quality gate before pushing. Add Proposed fix npm run typecheck || exit 1
+npm run lint || exit 1
+npm run format:check || exit 1
npm test || exit 1
+npm run build || exit 1📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Learnings |
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # Vendored / not part of this project's code | ||
| ref/ | ||
| .agents/ | ||
| .claude/ | ||
| skills-lock.json | ||
|
|
||
| # Build & deps | ||
| dist/ | ||
| coverage/ | ||
| node_modules/ | ||
| package-lock.json | ||
| *.lock | ||
|
|
||
| # Git hooks (shell, not prettier's domain) | ||
| .husky/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "semi": true, | ||
| "singleQuote": false, | ||
| "trailingComma": "all", | ||
| "printWidth": 100, | ||
| "tabWidth": 2, | ||
| "endOfLine": "lf" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # AGENTS.md — Rules for AI agents working on this repo | ||
|
|
||
| Hard rules. Follow exactly. These exist to keep agents from shipping broken or leaky code. | ||
|
|
||
| ## Secrets (highest priority) | ||
|
|
||
| - **Never commit secrets, API keys, tokens, or `.env` files.** Keys live only in `.env` (gitignored) or environment variables. | ||
| - **Never hardcode a key in source, tests, configs, or docs.** Read it from `DASHSCOPE_API_KEY` via `src/config.ts`. | ||
| - **Never paste a real key into a fixture.** Tests use dummy values (`sk-test`, `sk-secret-key-…`). The pre-commit `check-secrets.mjs` blocks `sk-ws-…` (real Bailian keys); don't try to evade it. | ||
| - If you accidentally stage a secret: unstage it, rotate the key immediately, and tell the maintainer. | ||
|
|
||
| ## Git hooks — never bypass | ||
|
|
||
| - **Never use `git commit --no-verify` or `git push --no-verify`.** Hooks run secret scan, lint, format, type-check, and tests for a reason. | ||
| - If a hook fails, fix the cause. Do not work around it. | ||
| - After first clone: run `npm install` (the `prepare` script installs husky hooks). Verify with `git config core.hooksPath` → `.husky`. | ||
|
|
||
| ## Quality gates — all must pass before push | ||
|
|
||
| Run these locally before considering work done: | ||
|
|
||
| ```bash | ||
| npm run typecheck # tsc --noEmit, strict + noUncheckedIndexedAccess + exactOptionalPropertyTypes | ||
| npm run lint # eslint, typescript-eslint strictTypeChecked, --max-warnings 0 | ||
| npm run format:check | ||
| npm test # vitest, unit + mocked e2e (live tests auto-skip without LIVE=1) | ||
| npm run build # tsc -p tsconfig.build.json -> dist/ | ||
| ``` | ||
|
|
||
| CI runs the same on Node 20 and 22. Local green ≠ CI green if you skip a step. | ||
|
|
||
| ## Code standards | ||
|
|
||
| - **TypeScript strict.** No `any` in `src/` (allowed sparingly in `test/` for fixture typing). No `@ts-ignore`. No non-null assertions in `src/`. | ||
| - Prefer narrow types and `unknown` over `any` when parsing external JSON (see `src/bailian.ts`). | ||
| - The DashScope payload builder (`buildPayload`) is intentionally injectable — if the `video_url`/`image_url` content block shape changes, change it in one place. | ||
| - Do not add a new runtime, language, or heavy dependency without explicit maintainer approval. | ||
| - Match existing style; let `prettier` and `eslint --fix` handle formatting. | ||
|
|
||
| ## Tool surface | ||
|
|
||
| The server exposes 8 MCP tools (see `src/server.ts`): `analyze_video`, `analyze_image`, `summarize_video`, `extract_video_text`, `video_qa`, `compare_video_frames`, `check_endpoint_status`, `list_capabilities`. Do not silently change a tool's name or argument schema — that breaks MCP clients. Add new tools rather than renaming. | ||
|
|
||
| `check_endpoint_status` must redact the API key (`redactKey`). There is a test asserting no key leaks — keep it passing. | ||
|
|
||
| ## Backend | ||
|
|
||
| - Endpoint: Bailian (DashScope) OpenAI-compatible mode, `${DASHSCOPE_BASE_URL}/chat/completions` (default `https://dashscope.aliyuncs.com/compatible-mode/v1`). | ||
| - Model: `qwen3.7-plus` (multimodal, native video — **no client-side frame extraction**). | ||
| - The Anthropic-compatible `/apps/anthropic` endpoint does NOT support video input. Do not switch to it for multimodal tools. | ||
| - Video frame sampling is server-side (fixed 0.5s/frame on OpenAI-compatible mode). Do not add frame extraction logic. | ||
|
|
||
| ## Testing | ||
|
|
||
| - Unit + mocked e2e use **msw** to mock `fetch` — no real API calls, no cost. Keep it that way. | ||
| - Live tests (`test/live.test.ts`) run only with `LIVE=1` and a real `DASHSCOPE_API_KEY`. They hit the real API and cost tokens. Run locally to verify behavior; never make them part of the default `npm test`. | ||
| - Every new tool or branch of logic gets a test. Coverage threshold is 85%. | ||
|
|
||
| ## Filesystem | ||
|
|
||
| - Delete files with `trash`, never `rm` (per global policy). | ||
| - `ref/` is vendored reference material — read-only, do not modify, do not import from. | ||
|
|
||
| ## Fragile assumptions (verify before relying on) | ||
|
|
||
| 1. The OpenAI-compatible endpoint accepts a `video_url` content block for `qwen3.7-plus`. If a live call rejects it, the fallback is the native DashScope `video` content type or switching to `qwen-vl-max-latest`. Change `contentBlock()` in `src/bailian.ts`. | ||
| 2. The exact model id string `qwen3.7-plus`. Verify against the Bailian model list if a call returns a model-not-found error. | ||
| 3. Local video >10MB cannot be sent as a base64 data URL (body too large). Host large local videos at a public URL instead. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| # qwen-omni-mcp | ||
|
|
||
| An [MCP](https://modelcontextprotocol.io) server that gives Claude Code and other AI agents **video and image understanding** via [Bailian (DashScope)](https://platform.qianwenai.com) using the multimodal **Qwen3.7-Plus** model. | ||
|
|
||
| Qwen3.7-Plus reads video natively — **no client-side frame extraction**. You pass a public media URL; the model does the rest. | ||
|
|
||
| ## Highlights | ||
|
|
||
| - **Native video understanding** — send a video URL, get grounded analysis | ||
| - **Image understanding** — describe, Q&A, OCR | ||
| - **Convenience tools** — summarize, text extraction, frame comparison, Q&A | ||
| - **npx-launchable** — one line in your MCP client config | ||
| - **Hardened** — secret-leak pre-commit guard + gitleaks, strict TypeScript, full CI | ||
|
|
||
| ## Install | ||
|
|
||
| No global install needed. Run directly with npx: | ||
|
|
||
| ```bash | ||
| npx -y qwen-omni-mcp | ||
| ``` | ||
|
|
||
| For local development: | ||
|
|
||
| ```bash | ||
| git clone <this-repo> | ||
| cd qwen-omni-mcp | ||
| npm install # also installs husky git hooks | ||
| cp .env.example .env # fill in DASHSCOPE_API_KEY | ||
| npm run dev # run from source via tsx | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| All config is via environment variables (loaded from `.env` by `dotenv`): | ||
|
|
||
| | Variable | Required | Default | Description | | ||
| | ---------------------- | -------- | --------------------------------------------------- | ------------------------------ | | ||
| | `DASHSCOPE_API_KEY` | yes | — | Bailian API key | | ||
| | `QWEN_MODEL` | no | `qwen3.7-plus` | Model id (multimodal) | | ||
| | `DASHSCOPE_BASE_URL` | no | `https://dashscope.aliyuncs.com/compatible-mode/v1` | OpenAI-compatible endpoint | | ||
| | `QWEN_REQUEST_TIMEOUT` | no | `300` | Per-request timeout in seconds | | ||
|
|
||
| Get a key at <https://platform.qianwenai.com/home/api-keys>. | ||
|
|
||
| > The Anthropic-compatible `/apps/anthropic` endpoint does **not** support video input, so this server uses the OpenAI-compatible endpoint. | ||
|
|
||
| ## Use with Claude Code | ||
|
|
||
| Add to your MCP client config: | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "qwen-omni-mcp": { | ||
| "command": "npx", | ||
| "args": ["-y", "qwen-omni-mcp"], | ||
| "env": { | ||
| "DASHSCOPE_API_KEY": "your-key" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| For local development without publishing: | ||
|
|
||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "qwen-omni-mcp": { | ||
| "command": "npx", | ||
| "args": ["tsx", "src/index.ts"], | ||
| "env": { "DASHSCOPE_API_KEY": "your-key" } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Tools | ||
|
|
||
| | Tool | Description | | ||
| | ----------------------- | ---------------------------------------------- | | ||
| | `analyze_video` | Analyze a video URL with a custom prompt | | ||
| | `analyze_image` | Analyze an image URL with a custom prompt | | ||
| | `summarize_video` | Brief / standard / detailed summary | | ||
| | `extract_video_text` | Extract on-screen text and transcribe speech | | ||
| | `video_qa` | Ask a specific question about a video | | ||
| | `compare_video_frames` | Analyze changes and progression across a video | | ||
| | `check_endpoint_status` | Show configured endpoint/model (key redacted) | | ||
| | `list_capabilities` | List server capabilities and supported formats | | ||
|
|
||
| Media must be reachable via a public `http`/`https` URL. Large local videos should be hosted at a public URL (base64 data URLs over ~10MB will be rejected). | ||
|
|
||
| ## Development | ||
|
|
||
| ```bash | ||
| npm run typecheck # strict tsc | ||
| npm run lint # eslint, --max-warnings 0 | ||
| npm run format:check # prettier | ||
| npm test # unit + mocked e2e (no API cost) | ||
| npm run build # emit dist/ | ||
| LIVE=1 npm run test:live # real API calls (costs tokens) | ||
| ``` | ||
|
|
||
| CI (`.github/workflows/ci.yml`) runs the same gates on Node 20/22. `secrets-scan.yml` runs gitleaks. `smoke-live.yml` (manual / weekly) runs one real image call. | ||
|
|
||
| See [AGENTS.md](AGENTS.md) for the full set of agent rules (never bypass hooks, never commit secrets, etc.). | ||
|
|
||
| ## License | ||
|
|
||
| MIT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use a top-level heading on the first line.
Markdownlint rule MD041 requires the first line to be an H1, so this template can fail the documented formatting gate.
Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 1-1: First line in a file should be a top-level heading
(MD041, first-line-heading, first-line-h1)
🤖 Prompt for AI Agents
Source: Linters/SAST tools