@@ -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 - z A - Z 0 - 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