@camscanner/mcp-language-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "i18n",
3
+ "description": "CamScanner multi-language string integration plugin. Search, export, and write locale strings via MCP language server.",
4
+ "version": "1.1.0",
5
+ "author": {
6
+ "name": "CamScanner"
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "language": {
3
+ "command": "npx",
4
+ "args": ["-y", "git+https://github.com/tianmuji/mcp-language-server.git"],
5
+ "env": {
6
+ "OPERATE_BASE_URL": "https://operate.intsig.net",
7
+ "SSO_LOGIN_URL": "https://web-sso.intsig.net/login",
8
+ "SSO_PLATFORM_ID": "OdliDeAnVtlUA5cGwwxZPHUyXtqPCcNw",
9
+ "SSO_CALLBACK_DOMAIN": "https://www-sandbox.camscanner.com/activity/mcp-auth-callback",
10
+ "SSO_CALLBACK_PORT": "9877"
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: i18n
3
+ description: "多语言字符串集成助手。当用户要求集成、查询、替换多语言字符串时触发,或用户提到 i18n、多语言、$t、国际化、翻译等关键词时触发。"
4
+ argument-hint: <要集成的中文字符串>
5
+ disable-model-invocation: false
6
+ ---
7
+
8
+ # 多语言字符串集成助手
9
+
10
+ 帮助用户查询、集成多语言字符串到项目代码中。支持多个产品、多个平台、多种业务线。
11
+
12
+ ## 可用 MCP 工具
13
+
14
+ 来自 language MCP server:
15
+
16
+ 1. **list-products** — 查询所有产品列表(产品名 → product_id)
17
+ 2. **list-platforms** — 查询所有平台列表(平台名 → platform_id)
18
+ 3. **get-version-list** — 获取指定产品的版本列表
19
+ 4. **search-string** — 按关键词搜索多语言字符串(远程多语言平台)
20
+ 5. **export-string** — 导出为兼容 cs-i18n 的 locale JSON 格式
21
+ 6. **write-locales** — 将字符串写入项目的 locales 目录
22
+
23
+ ## 产品与平台
24
+
25
+ > **不要硬编码产品或平台 ID。** 使用 `list-products` 和 `list-platforms` 工具动态查询。
26
+ >
27
+ > 不同产品和平台的字符串 key 可能不同。同一条字符串在 Android/iOS/Web 上可能有不同的 key 名称。
28
+
29
+ ## 核心原则
30
+
31
+ 1. **不假设任何项目配置** — 不要硬编码 product_id、platform_id 或 locale 路径
32
+ 2. **先了解再实现** — 先检测项目环境,再操作
33
+ 3. **不确定就问** — 产品、平台、版本不确定时,询问用户
34
+
35
+ ## 工作流程
36
+
37
+ 当用户请求集成多语言字符串时,**严格按以下步骤执行**:
38
+
39
+ ### 第 0 步:确认产品与平台
40
+
41
+ 如果用户没有明确指定产品和平台:
42
+
43
+ 1. 调用 **list-products** 获取产品列表,展示给用户选择
44
+ 2. 调用 **list-platforms** 获取平台列表,让用户确认目标平台
45
+ 3. 如果用户已在之前的对话中指定过,或项目 CLAUDE.md 中有记录,则直接使用
46
+
47
+ 记住用户选择的 product_id 和 platform_id,在后续步骤中使用。
48
+
49
+ ### 第 1 步:检测项目多语言目录
50
+
51
+ **不要假设多语言文件在 `src/locales/`**。需要动态检测当前项目的 locale 目录:
52
+
53
+ 1. **查找 i18n 配置** — 搜索项目中的 i18n 配置文件(如 `i18n.ts`、`i18n.js`、`vue.config.*` 等),确认 locale 文件路径
54
+ 2. **搜索 locale 文件** — 使用 Glob 搜索 `**/{locales,locale,lang,i18n}/*.json` 或类似模式
55
+ 3. **验证目录结构** — 确认找到的目录包含语言 JSON 文件(如 `ZhCn.json`、`EnUs.json`、`zh-CN.json` 等)
56
+ 4. **确认主参考文件** — 找到中文语言文件作为本地搜索的参考
57
+
58
+ 如果找不到或有多个候选目录,**询问用户确认**。
59
+
60
+ 将检测到的路径记为 `LOCALES_DIR`,后续步骤使用。
61
+
62
+ ### 第 2 步:了解现有实现
63
+
64
+ 1. **阅读用户指定的组件/页面代码**,理解当前的 i18n 用法和代码风格
65
+ 2. 查看同模块中已有的 `$t()` / `t()` / `i18n.t()` 等调用,确认命名模式和使用惯例
66
+ 3. 确认项目使用的 i18n 框架和参数占位符格式(`{0}` / `{name}` / `%s` 等)
67
+
68
+ ### 第 3 步:本地查找(优先)
69
+
70
+ 先在项目本地 locale 文件中搜索,检查是否已存在匹配的字符串:
71
+
72
+ 1. 使用 **Grep 工具**在中文语言文件中搜索用户提供的中文字符串
73
+ 2. **严格匹配规则**: value 必须与用户需要的字符串**完全一致**
74
+ 3. 如果本地找到了完全匹配的 key → **直接使用该 key,跳到第 5 步**
75
+
76
+ ### 第 4 步:远程查询(本地未找到时)
77
+
78
+ 使用 `search-string` 从远程平台查询:
79
+
80
+ - `product_id` 使用第 0 步确认的值
81
+ - **必须使用精确匹配**: `fuzzy: "0"`
82
+ - 将结果整理为表格展示(版本、key、中文、英文、繁体)
83
+ - 无匹配结果 → 告知用户需要在多语言平台新增
84
+
85
+ #### 搜索策略优先级
86
+
87
+ 精确匹配同时匹配 key 名和中文值。按以下优先级搜索:
88
+
89
+ 1. **按 key 名搜索**(最可靠)— 如果已知 key 名(如 `cs_519b_selected_some`),直接用 key 名精确搜索
90
+ 2. **按中文值精确搜索** — 适用于不带参数的简单字符串(如 `删除`、`上传图片`)
91
+ 3. **模糊搜索 + 人工确认** — 上述方式无结果时的兜底方案
92
+
93
+ #### 带参数字符串的注意事项
94
+
95
+ 远程平台的占位符格式不统一,同一字符串在不同版本中可能是 `%s`、`%d`、`{0}`,空格也可能不一致。
96
+ 例如 `cs_519b_selected_some` 在远程各版本中分别存为:`已选择%s条`、`已选择{0}条`、`已选择 {0} 条`。
97
+
98
+ 因此**带参数的字符串不要用中文值精确搜索**,应该用 key 名搜索。如果不知道 key 名:
99
+ 1. 先用模糊搜索 (`fuzzy: "1"`) 定位候选结果
100
+ 2. 从结果中确认正确的 key 名
101
+ 3. 再用 key 名精确搜索获取完整翻译
102
+
103
+ ### 第 5 步:写入本地 & 替换代码
104
+
105
+ 1. 检查本地是否已有该 key
106
+ 2. 如果没有:使用 `write-locales` 写入
107
+ - `locales_path` 使用第 1 步检测到的 `LOCALES_DIR` **绝对路径**
108
+ - `product_id` 使用第 0 步确认的值
109
+ - `platform_id` 使用第 0 步确认的值
110
+ - **默认精确匹配** (`fuzzy: "0"`)
111
+ - 搜索词优先使用 **key 名**(尤其是带参数的字符串)
112
+ 3. 替换代码中的硬编码字符串为 i18n 调用,**遵循第 2 步中了解到的现有代码风格**
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "yapi",
3
+ "description": "YApi API documentation management plugin. Search, query, create, and export API documentation via YApi MCP server.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "CamScanner"
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "yapi": {
3
+ "command": "npx",
4
+ "args": ["-y", "git+https://gitlab.intsig.net/cs-templates/skills/yapi-mcp-server.git"],
5
+ "env": {
6
+ "YAPI_BASE_URL": "https://web-api.intsig.net",
7
+ "SSO_LOGIN_URL": "https://web-sso.intsig.net/login",
8
+ "SSO_PLATFORM_ID": "odVOyexj6maKIHAXv9LflO8tw7WNOI4I",
9
+ "SSO_CALLBACK_DOMAIN": "https://static-cdn.camscanner.com/camscanner-activity/mcp-auth-callback.html",
10
+ "SSO_CALLBACK_PORT": "9876"
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: yapi
3
+ description: "YApi API文档管理助手。当用户要求查询、搜索、创建、更新、导出API接口文档时触发,或用户提到 YApi、接口文档、API文档、接口定义、swagger 等关键词时触发。"
4
+ argument-hint: <API名称或关键词>
5
+ disable-model-invocation: false
6
+ ---
7
+
8
+ # YApi API文档管理助手
9
+
10
+ 帮助用户通过 YApi MCP Server 查询和管理云端 API 接口文档。
11
+
12
+ ## 可用 MCP 工具
13
+
14
+ 来自 yapi MCP server:
15
+
16
+ 1. **yapi-auth** — SSO 登录(首次使用需要扫码认证)
17
+ 2. **yapi-logout** — 退出登录,清除凭证
18
+ 3. **list_projects** — 列出用户有权限的所有分组和项目
19
+ 4. **list_apis** — 获取项目下所有 API 接口(按分类分组)
20
+ 5. **search_api** — 按路径或标题关键词搜索 API 接口
21
+ 6. **get_api_detail** — 获取单个 API 的完整定义(参数、请求体、响应体)
22
+ 7. **get_project_info** — 获取项目基本信息
23
+ 8. **export_swagger** — 导出 Swagger/OpenAPI 格式文档
24
+ 9. **import_api_docs** — 批量导入完整 API 文档(支持按分类或关键词过滤)
25
+ 10. **create_api** — 创建新 API 接口
26
+ 11. **update_api** — 更新已有 API 接口
27
+
28
+ ## 核心原则:先了解再实现
29
+
30
+ 在对接 API 接口之前,**必须先了解当前项目中相关功能的现有实现**:
31
+
32
+ 1. **阅读相关代码** — 找到用户要修改的模块,理解它的 API 调用方式、数据流、错误处理等
33
+ 2. **查看周边实现** — 看同模块其他接口是怎么封装的(axios 实例、请求/响应拦截、类型定义),保持一致
34
+ 3. **再查 YApi 文档** — 基于对现有代码的理解,从 YApi 获取接口定义,对比差异后再动手
35
+ 4. **PRD 与接口冲突时** — 如果 PRD 描述与 YApi 接口定义不一致,向用户确认以哪个为准
36
+
37
+ ## 工作流程
38
+
39
+ 当用户请求查询或操作 API 文档时,**按以下步骤执行**:
40
+
41
+ ### 第 0 步:了解现有实现
42
+
43
+ 1. **阅读用户指定的模块/页面代码**,理解当前的 API 调用方式和数据结构
44
+ 2. 查看 `src/api/` 目录下相关的接口封装,确认命名规范、请求方式、类型定义
45
+ 3. 了解相关 Store 和组件如何消费接口数据
46
+ 4. 这一步不可跳过 — 不理解上下文就对接接口会导致风格不一致或结构错误
47
+
48
+ ### 第 1 步:认证检查
49
+
50
+ 如果尚未认证,先调用 `yapi-auth` 进行登录。
51
+
52
+ ### 第 2 步:确定项目
53
+
54
+ - 如果用户未指定项目,使用 `list_projects` 列出可用项目让用户选择
55
+ - 如果用户提供了 project_id,直接使用
56
+
57
+ ### 第 3 步:查询接口
58
+
59
+ 根据用户需求选择合适的工具:
60
+
61
+ - **搜索特定接口**: 使用 `search_api`,提供 project_id 和关键词
62
+ - **浏览所有接口**: 使用 `list_apis` 获取完整接口列表
63
+ - **查看接口详情**: 使用 `get_api_detail` 获取完整定义(包含请求参数、请求体、响应体 schema)
64
+ - **批量获取文档**: 使用 `import_api_docs` 一次性获取多个接口的完整文档
65
+
66
+ ### 第 4 步:展示结果
67
+
68
+ - 以清晰的格式展示接口信息
69
+ - 包含 HTTP 方法、路径、参数说明、请求体/响应体结构
70
+ - 如果有环境域名信息,展示完整 URL
71
+ - **与第 0 步了解到的现有代码进行对比**,指出差异点
72
+
73
+ ### 创建/更新接口
74
+
75
+ - **创建接口**: 使用 `create_api`,需要 project_id、cat_id、title、path、method
76
+ - **更新接口**: 使用 `update_api`,需要 interface_id,只修改指定字段
package/setup.sh ADDED
@@ -0,0 +1,96 @@
1
+ #!/bin/bash
2
+ # CamScanner i18n MCP Server + Plugin 一键安装
3
+ # 用法: bash setup.sh
4
+ set -e
5
+
6
+ echo "=== CamScanner i18n 多语言工具安装 ==="
7
+ echo ""
8
+
9
+ # ---- 配置 ----
10
+ MCP_SERVER_DIR="$HOME/mcp-language-server"
11
+
12
+ # ---- 1. 安装 MCP Server ----
13
+ echo "[1/3] 安装 MCP Language Server..."
14
+ if [ -d "$MCP_SERVER_DIR" ]; then
15
+ echo " 目录已存在,更新中..."
16
+ cd "$MCP_SERVER_DIR"
17
+ git pull 2>/dev/null || echo " 非 git 仓库,跳过更新"
18
+ else
19
+ echo " 克隆仓库..."
20
+ git clone git@github.com:tianmuji/mcp-language-server.git "$MCP_SERVER_DIR"
21
+ fi
22
+
23
+ cd "$MCP_SERVER_DIR"
24
+ if [ -f "package.json" ]; then
25
+ echo " 安装依赖..."
26
+ npm install --silent 2>/dev/null
27
+ echo " 依赖安装完成"
28
+ fi
29
+
30
+ # ---- 2. 配置 Cookie ----
31
+ echo ""
32
+ echo "[2/3] 配置认证信息..."
33
+ if [ ! -f "$MCP_SERVER_DIR/.cookie" ]; then
34
+ echo ""
35
+ echo " 需要配置 Cookie 才能访问多语言平台。"
36
+ echo " 获取方式:"
37
+ echo " 1. 浏览器打开 https://operate-test.intsig.net/multilanguage"
38
+ echo " 2. 登录后打开 DevTools → Network"
39
+ echo " 3. 做一次搜索操作,找到 get-string-search 请求"
40
+ echo " 4. 右键 → Copy → Copy as cURL"
41
+ echo " 5. 从 curl 中提取 -b 后面的 cookie 值"
42
+ echo ""
43
+ read -p " 粘贴 Cookie 值 (或回车跳过): " COOKIE_VALUE
44
+ if [ -n "$COOKIE_VALUE" ]; then
45
+ echo "$COOKIE_VALUE" > "$MCP_SERVER_DIR/.cookie"
46
+ echo " Cookie 已保存"
47
+ else
48
+ echo " 跳过,稍后请手动写入 $MCP_SERVER_DIR/.cookie"
49
+ fi
50
+
51
+ echo ""
52
+ read -p " 粘贴 X-CSRF-Token 值 (或回车跳过): " CSRF_VALUE
53
+ if [ -n "$CSRF_VALUE" ]; then
54
+ echo "$CSRF_VALUE" > "$MCP_SERVER_DIR/.csrf-token"
55
+ echo " CSRF Token 已保存"
56
+ else
57
+ echo " 跳过,稍后请手动写入 $MCP_SERVER_DIR/.csrf-token"
58
+ fi
59
+ else
60
+ echo " Cookie 已存在,跳过"
61
+ fi
62
+
63
+ # ---- 3. 安装 i18n Plugin (自动注册 MCP Server + /i18n Skill) ----
64
+ echo ""
65
+ echo "[3/3] 安装 i18n Plugin..."
66
+
67
+ # 添加 marketplace
68
+ if claude plugin marketplace list 2>&1 | grep -q "camscanner-plugins"; then
69
+ echo " Marketplace 已注册,跳过"
70
+ else
71
+ echo " 注册 marketplace..."
72
+ claude plugin marketplace add tianmuji/mcp-language-server --sparse plugins .claude-plugin 2>&1
73
+ fi
74
+
75
+ # 安装插件
76
+ if claude plugin list 2>&1 | grep -q "i18n@camscanner-plugins"; then
77
+ echo " Plugin 已安装,跳过"
78
+ else
79
+ echo " 安装插件..."
80
+ claude plugin install i18n 2>&1
81
+ fi
82
+
83
+ # ---- 完成 ----
84
+ echo ""
85
+ echo "==============================="
86
+ echo " 安装完成!请重启 Claude Code"
87
+ echo "==============================="
88
+ echo ""
89
+ echo "使用方式:"
90
+ echo " /i18n 下载 → 自动搜索+集成多语言字符串"
91
+ echo " /i18n 扫描全能王 → 搜索指定字符串"
92
+ echo ""
93
+ echo "更新 Cookie(过期后):"
94
+ echo " 编辑 $MCP_SERVER_DIR/.cookie"
95
+ echo " 编辑 $MCP_SERVER_DIR/.csrf-token"
96
+ echo ""
package/src/auth.ts ADDED
@@ -0,0 +1,158 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import { chromium } from 'playwright-core'
5
+ import type { OperateCredentials } from './operate-client.js'
6
+
7
+ // --- Credentials persistence ---
8
+
9
+ const CREDS_DIR = path.join(os.homedir(), '.language-mcp')
10
+ const CREDS_FILE = path.join(CREDS_DIR, 'credentials.json')
11
+
12
+ export async function loadCredentials(): Promise<OperateCredentials | null> {
13
+ try {
14
+ if (!fs.existsSync(CREDS_FILE)) return null
15
+ const data = JSON.parse(fs.readFileSync(CREDS_FILE, 'utf-8'))
16
+ if (data && data.expiresAt > Date.now()) return data
17
+ return null
18
+ } catch {
19
+ return null
20
+ }
21
+ }
22
+
23
+ export async function saveCredentials(creds: OperateCredentials): Promise<void> {
24
+ if (!fs.existsSync(CREDS_DIR)) fs.mkdirSync(CREDS_DIR, { recursive: true })
25
+ fs.writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2))
26
+ }
27
+
28
+ export async function clearCredentials(): Promise<void> {
29
+ try { fs.unlinkSync(CREDS_FILE) } catch { /* ignore */ }
30
+ }
31
+
32
+ // --- Find system Chromium installed by Playwright ---
33
+
34
+ function findChromium(): string | undefined {
35
+ const cacheDir = path.join(os.homedir(), 'Library', 'Caches', 'ms-playwright')
36
+ if (!fs.existsSync(cacheDir)) return undefined
37
+
38
+ const dirs = fs.readdirSync(cacheDir)
39
+ .filter(d => d.startsWith('chromium-'))
40
+ .sort()
41
+ .reverse()
42
+
43
+ for (const dir of dirs) {
44
+ const candidates = [
45
+ path.join(cacheDir, dir, 'chrome-mac-arm64', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'),
46
+ path.join(cacheDir, dir, 'chrome-mac', 'Google Chrome for Testing.app', 'Contents', 'MacOS', 'Google Chrome for Testing'),
47
+ path.join(cacheDir, dir, 'chrome-mac-arm64', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'),
48
+ path.join(cacheDir, dir, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'),
49
+ path.join(cacheDir, dir, 'chrome-linux', 'chrome'),
50
+ ]
51
+ for (const c of candidates) {
52
+ if (fs.existsSync(c)) return c
53
+ }
54
+ }
55
+ return undefined
56
+ }
57
+
58
+ // --- Auth config ---
59
+
60
+ export interface SsoConfig {
61
+ operateBaseUrl: string
62
+ }
63
+
64
+ /**
65
+ * Launch a browser for the user to complete the full login flow
66
+ * (SSO + zero-trust gateway), then extract all cookies and CSRF token.
67
+ */
68
+ export async function startSsoLogin(config: SsoConfig): Promise<OperateCredentials> {
69
+ const execPath = findChromium()
70
+ if (!execPath) {
71
+ throw new Error(
72
+ 'Cannot find Chromium. Please install Playwright browsers: npx playwright install chromium'
73
+ )
74
+ }
75
+
76
+ // Persistent browser data dir — saves passwords, cookies across sessions
77
+ const userDataDir = path.join(CREDS_DIR, 'browser-data')
78
+ if (!fs.existsSync(userDataDir)) fs.mkdirSync(userDataDir, { recursive: true })
79
+
80
+ // Navigate to a page that requires auth — this triggers the SSO redirect chain
81
+ const targetUrl = config.operateBaseUrl + '/multilanguage/edit-language'
82
+
83
+ console.error('[Auth] Launching browser for login...')
84
+ const context = await chromium.launchPersistentContext(userDataDir, {
85
+ headless: false,
86
+ executablePath: execPath,
87
+ ignoreHTTPSErrors: true,
88
+ })
89
+
90
+ try {
91
+ const page = context.pages()[0] || await context.newPage()
92
+
93
+ await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 15000 })
94
+
95
+ // If already logged in (browser has valid cookies from previous session), skip waiting
96
+ const currentHost = new URL(page.url()).hostname
97
+ if (currentHost !== 'operate.intsig.net') {
98
+ console.error('[Auth] Waiting for user to complete login (up to 180s)...')
99
+ await page.waitForURL(url => {
100
+ const u = typeof url === 'string' ? new URL(url) : url
101
+ return u.hostname === 'operate.intsig.net'
102
+ }, { timeout: 180000 })
103
+ }
104
+
105
+ // Ensure the page is fully loaded
106
+ await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {})
107
+
108
+ // Extract all cookies
109
+ const cookies = await context.cookies()
110
+ const operateCookies = cookies.filter(c =>
111
+ c.domain.includes('intsig.net') || c.domain.includes('operate')
112
+ )
113
+
114
+ if (operateCookies.length === 0) {
115
+ throw new Error('No cookies captured after login. Please try again.')
116
+ }
117
+
118
+ // Navigate to /site/get-config to extract CSRF token
119
+ await page.goto(config.operateBaseUrl + '/site/get-config', { waitUntil: 'domcontentloaded', timeout: 15000 })
120
+
121
+ // Re-capture cookies (get-config may refresh JSESSID)
122
+ const allCookies = (await context.cookies()).filter(c =>
123
+ c.domain.includes('intsig.net') || c.domain.includes('operate')
124
+ )
125
+ const finalCookie = allCookies.map(c => `${c.name}=${c.value}`).join('; ')
126
+
127
+ let csrfToken = ''
128
+ try {
129
+ csrfToken = await page.evaluate(`
130
+ (() => {
131
+ const el = document.querySelector('input[name="_csrf"]');
132
+ return el ? el.value : '';
133
+ })()
134
+ `) as string
135
+ } catch { /* ignore */ }
136
+
137
+ if (!csrfToken) {
138
+ const html = await page.content()
139
+ const match = html.match(/name="_csrf"\s+value="([^"]+)"/)
140
+ if (match) csrfToken = match[1]
141
+ }
142
+
143
+ if (!csrfToken) {
144
+ throw new Error('Login succeeded but failed to extract CSRF token. Please try again.')
145
+ }
146
+
147
+ console.error('[Auth] Login successful! Cookies and CSRF token captured.')
148
+
149
+ return {
150
+ ssoToken: '',
151
+ sessionCookie: finalCookie,
152
+ csrfToken,
153
+ expiresAt: Date.now() + 24 * 60 * 60 * 1000,
154
+ }
155
+ } finally {
156
+ await context.close()
157
+ }
158
+ }