Skip to content

Commit 3712fd2

Browse files
authored
feat(yapi): global/current MCP config buttons (#28)
* feat(yapi): add global/current MCP config buttons * docs: clarify YApi MCP buttons * feat(yapi): all-projects MCP config with email autofill * fix(yapi): validate email before injecting into MCP config * fix(yapi): autofill email via user/status for all-projects config * fix: avoid breaking non-yapi pages * chore(yapi): use @leeguoo/yapi-auto-mcp package * chore(yapi): switch MCP package to @leeguoo/yapi-mcp
1 parent 74a4279 commit 3712fd2

File tree

2 files changed

+184
-22
lines changed

2 files changed

+184
-22
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ cd cross-request-master
3737
安装后直接在 YApi 页面发送请求即可,扩展会自动处理跨域、显示 cURL,并把 JSON 响应解析为对象供脚本使用。
3838

3939
在接口详情页(基本信息区域右上角)额外提供:
40-
- **MCP 配置**:自动拼好 Cursor / Codex / Gemini CLI / Claude Code 的配置并可一键复制
41-
- **复制给 AI**:把当前接口信息整理成 Markdown(仅接口相关字段)复制到剪贴板
40+
- **所有项目 MCP 配置**:全局模式(账号密码登录),只需配置一次,后续可自动缓存所有项目 token(生成配置默认使用 `@leeguoo/yapi-mcp`
41+
- **当前项目 MCP 配置**:项目 token 模式(自动拼好 `projectId:token`),适合单项目/少量项目
42+
- **复制当前页面给 AI**:把当前接口信息整理成 Markdown(仅接口相关字段)复制到剪贴板
4243

4344
### YApi OpenAPI(Yapi-MCP tool 同名方法)
4445

content-script.js

Lines changed: 181 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,15 @@ const CrossRequest = {
215215
return true;
216216
};
217217

218+
const looksLikeEmail = (val) => {
219+
if (typeof val !== 'string') return false;
220+
const s = val.trim();
221+
if (!s) return false;
222+
if (s.length > 128) return false;
223+
if (/[\s"'<>]/.test(s)) return false;
224+
return /^[^@]+@[^@]+\.[^@]+$/.test(s);
225+
};
226+
218227
const findTokenInObject = (obj) => {
219228
if (!obj || typeof obj !== 'object') return '';
220229
const queue = [{ value: obj, depth: 0, key: '' }];
@@ -373,6 +382,7 @@ const CrossRequest = {
373382
};
374383

375384
const buildMcpConfigBlocks = ({ origin, projectId, projectName, token }) => {
385+
const mcpPkg = '@leeguoo/yapi-mcp';
376386
const baseUrl = String(origin || '').replace(/\/$/, '');
377387
const yapiToken = `${projectId}:${token}`;
378388
const normalizedProjectName = String(projectName || '')
@@ -385,7 +395,7 @@ const CrossRequest = {
385395

386396
const stdioArgs = [
387397
'-y',
388-
'yapi-auto-mcp',
398+
mcpPkg,
389399
'--stdio',
390400
`--yapi-base-url=${baseUrl}`,
391401
`--yapi-token=${yapiToken}`
@@ -441,6 +451,124 @@ const CrossRequest = {
441451
serverName
442452
};
443453
};
454+
const buildGlobalMcpConfigBlocks = ({ origin, email }) => {
455+
const mcpPkg = '@leeguoo/yapi-mcp';
456+
const baseUrl = String(origin || '').replace(/\/$/, '');
457+
const host = String(location.hostname || 'yapi').replace(/[^a-zA-Z0-9._-]/g, '');
458+
const serverName = `yapi-global-${host.replace(/\./g, '-')}-mcp`;
459+
const cliServerName = /\s/.test(serverName) ? JSON.stringify(serverName) : serverName;
460+
const safeEmail = looksLikeEmail(email) ? String(email).trim() : 'YOUR_EMAIL';
461+
462+
const stdioArgs = [
463+
'-y',
464+
mcpPkg,
465+
'--stdio',
466+
`--yapi-base-url=${baseUrl}`,
467+
'--yapi-auth-mode=global',
468+
`--yapi-email=${safeEmail}`,
469+
'--yapi-password=YOUR_PASSWORD'
470+
];
471+
472+
const cursor = JSON.stringify(
473+
{
474+
mcpServers: {
475+
[serverName]: {
476+
command: 'npx',
477+
args: stdioArgs
478+
}
479+
}
480+
},
481+
null,
482+
2
483+
);
484+
485+
const codex = `[mcp_servers.${JSON.stringify(serverName)}]\ncommand = "npx"\nargs = ${JSON.stringify(
486+
stdioArgs
487+
)}\n`;
488+
489+
const gemini = JSON.stringify(
490+
{
491+
mcpServers: {
492+
[serverName]: {
493+
command: 'npx',
494+
args: stdioArgs
495+
}
496+
}
497+
},
498+
null,
499+
2
500+
);
501+
502+
const claudeCode = `claude mcp add --transport stdio ${cliServerName} -- npx ${stdioArgs
503+
.map((a) => (a.includes(' ') ? JSON.stringify(a) : a))
504+
.join(' ')}`;
505+
506+
const geminiCli = `gemini mcp add --transport stdio ${cliServerName} npx ${stdioArgs
507+
.map((a) => (a.includes(' ') ? JSON.stringify(a) : a))
508+
.join(' ')}`;
509+
510+
const rawCommand = `npx ${stdioArgs.join(' ')}`;
511+
512+
return {
513+
cursor,
514+
codex,
515+
gemini,
516+
claudeCode,
517+
geminiCli,
518+
rawCommand,
519+
serverName
520+
};
521+
};
522+
523+
const getCookieValue = (key) => {
524+
const name = `${String(key || '').trim()}=`;
525+
if (!name || name === '=') return '';
526+
const cookies = String(document.cookie || '').split(';');
527+
for (const c of cookies) {
528+
const trimmed = String(c || '').trim();
529+
if (!trimmed.startsWith(name)) continue;
530+
return trimmed.slice(name.length);
531+
}
532+
return '';
533+
};
534+
535+
const resolveCurrentUserEmail = async (origin) => {
536+
// 1) 优先用 status(不依赖读取 cookie;只要 fetch 带 credentials 即可)
537+
try {
538+
const statusUrl = `${origin}/api/user/status`;
539+
const statusPayload = await fetchJson(statusUrl);
540+
const emailFromStatus = statusPayload && statusPayload.data && statusPayload.data.email;
541+
if (looksLikeEmail(emailFromStatus)) return String(emailFromStatus).trim();
542+
543+
const uidFromStatus =
544+
statusPayload && statusPayload.data && (statusPayload.data.uid || statusPayload.data._id);
545+
if (uidFromStatus) {
546+
const url = `${origin}/api/user/find?id=${encodeURIComponent(String(uidFromStatus))}`;
547+
const payload = await fetchJson(url);
548+
if (
549+
payload &&
550+
payload.errcode === 0 &&
551+
payload.data &&
552+
looksLikeEmail(payload.data.email)
553+
) {
554+
return String(payload.data.email || '').trim();
555+
}
556+
}
557+
} catch {
558+
// ignore
559+
}
560+
561+
// 2) 兜底:尝试从非 HttpOnly 的 _yapi_uid 读取
562+
const uid = getCookieValue('_yapi_uid');
563+
if (!uid) return '';
564+
const url = `${origin}/api/user/find?id=${encodeURIComponent(uid)}`;
565+
const payload = await fetchJson(url);
566+
if (payload && payload.errcode === 0 && payload.data && looksLikeEmail(payload.data.email)) {
567+
return String(payload.data.email || '').trim();
568+
}
569+
return '';
570+
};
571+
444572
const isTruthyRequired = (val) => {
445573
if (val === true) return true;
446574
if (val === 1) return true;
@@ -791,8 +919,8 @@ const CrossRequest = {
791919
</div>
792920
<div class="crm-body">
793921
<div class="crm-section">
794-
<h3>MCP 配置</h3>
795-
<div class="crm-hint">已按当前项目自动拼好(Cursor / Codex / Gemini CLI / Claude Code)。</div>
922+
<h3 id="crm-mcp-title">MCP 配置</h3>
923+
<div class="crm-hint" id="crm-mcp-hint">已按当前项目自动拼好(Cursor / Codex / Gemini CLI / Claude Code)。</div>
796924
<div id="crm-mcp-content" style="margin-top: 10px;"></div>
797925
</div>
798926
</div>
@@ -831,30 +959,56 @@ const CrossRequest = {
831959
return container;
832960
};
833961

834-
const openModal = async () => {
962+
const openModal = async (mode) => {
835963
const route = parseYapiInterfaceRoute();
836964
if (!route) return;
837965

838966
ensureStyle();
839967
const modal = ensureModal();
840968
modal.style.display = 'block';
841969

970+
const headerTitle = modal.querySelector('.crm-title');
971+
const panelTitle = modal.querySelector('#crm-mcp-title');
972+
const panelHint = modal.querySelector('#crm-mcp-hint');
973+
842974
const mcpContainer = modal.querySelector('#crm-mcp-content');
843975
mcpContainer.textContent = '生成中...';
844976

845977
const origin = location.origin;
846978

847979
try {
848-
const [token, projectName] = await Promise.all([
849-
resolveProjectToken(origin, route.projectId),
850-
resolveProjectName(origin, route.projectId)
851-
]);
852-
const blocks = buildMcpConfigBlocks({
853-
origin,
854-
projectId: route.projectId,
855-
projectName,
856-
token
857-
});
980+
const mcpMode = mode === 'global' ? 'global' : 'project';
981+
if (mcpMode === 'global') {
982+
if (headerTitle) headerTitle.textContent = 'MCP 配置(所有项目)';
983+
if (panelTitle) panelTitle.textContent = 'MCP 配置(所有项目)';
984+
if (panelHint) {
985+
panelHint.textContent =
986+
'全局模式:邮箱会尽量自动填入;只需填写密码。启动后先在对话里调用一次 yapi_update_token 自动缓存所有项目 token。';
987+
}
988+
} else {
989+
if (headerTitle) headerTitle.textContent = 'MCP 配置(当前项目)';
990+
if (panelTitle) panelTitle.textContent = 'MCP 配置(当前项目)';
991+
if (panelHint)
992+
panelHint.textContent =
993+
'已按当前项目自动拼好(Cursor / Codex / Gemini CLI / Claude Code)。';
994+
}
995+
996+
let blocks;
997+
if (mcpMode === 'global') {
998+
const email = await resolveCurrentUserEmail(origin);
999+
blocks = buildGlobalMcpConfigBlocks({ origin, email });
1000+
} else {
1001+
const [token, projectName] = await Promise.all([
1002+
resolveProjectToken(origin, route.projectId),
1003+
resolveProjectName(origin, route.projectId)
1004+
]);
1005+
blocks = buildMcpConfigBlocks({
1006+
origin,
1007+
projectId: route.projectId,
1008+
projectName,
1009+
token
1010+
});
1011+
}
8581012

8591013
mcpContainer.textContent = '';
8601014
mcpContainer.style.display = 'block';
@@ -918,19 +1072,26 @@ const CrossRequest = {
9181072
const group = document.createElement('span');
9191073
group.id = BTN_GROUP_ID;
9201074

921-
const mcpBtn = document.createElement('button');
922-
mcpBtn.className = 'crm-btn';
923-
mcpBtn.type = 'button';
924-
mcpBtn.textContent = 'MCP 配置';
925-
mcpBtn.addEventListener('click', openModal);
1075+
const mcpGlobalBtn = document.createElement('button');
1076+
mcpGlobalBtn.className = 'crm-btn';
1077+
mcpGlobalBtn.type = 'button';
1078+
mcpGlobalBtn.textContent = '所有项目 MCP 配置';
1079+
mcpGlobalBtn.addEventListener('click', () => openModal('global'));
1080+
1081+
const mcpProjectBtn = document.createElement('button');
1082+
mcpProjectBtn.className = 'crm-btn';
1083+
mcpProjectBtn.type = 'button';
1084+
mcpProjectBtn.textContent = '当前项目 MCP 配置';
1085+
mcpProjectBtn.addEventListener('click', () => openModal('project'));
9261086

9271087
const copyBtn = document.createElement('button');
9281088
copyBtn.className = 'crm-btn crm-primary';
9291089
copyBtn.type = 'button';
9301090
copyBtn.textContent = '复制当前页面给 AI';
9311091
copyBtn.addEventListener('click', () => copyMarkdownDirectly(copyBtn));
9321092

933-
group.appendChild(mcpBtn);
1093+
group.appendChild(mcpGlobalBtn);
1094+
group.appendChild(mcpProjectBtn);
9341095
group.appendChild(copyBtn);
9351096
titleEl.appendChild(group);
9361097
};

0 commit comments

Comments
 (0)