mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-13 19:20:04 +00:00
🐛 fix(codex): reuse Linear inspector for MCP calls (#15738)
* 🐛 fix(codex): reuse Linear inspector for MCP calls * 🐛 fix(codex): gate generic Linear MCP labels
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
getMcpToolName,
|
||||
} from './mcpToolUtils';
|
||||
|
||||
const LINEAR_TOOL_NAME_SET = new Set<string>(LINEAR_TOOL_NAMES);
|
||||
const LINEAR_TOOL_NAME_SET = new Set<string>([...LINEAR_TOOL_NAMES, 'fetch', 'search']);
|
||||
const SharedLinearInspector = LinearInspector as ComponentType<
|
||||
BuiltinInspectorProps<Record<string, unknown>>
|
||||
>;
|
||||
@@ -33,17 +33,23 @@ const McpToolInspector = memo<BuiltinInspectorProps<CodexMcpToolArgs, CodexMcpTo
|
||||
});
|
||||
const server = getMcpServer(args, pluginState) || getMcpServer(partialArgs);
|
||||
const tool = getMcpToolName(args, pluginState) || getMcpToolName(partialArgs);
|
||||
const linearApiName = getCodexLinearMcpApiName(tool);
|
||||
const input = getMcpInputRecord(args, pluginState);
|
||||
const partialInput = getMcpInputRecord(partialArgs);
|
||||
const linearApiName = getCodexLinearMcpApiName({
|
||||
input: input || partialInput,
|
||||
server,
|
||||
toolName: tool,
|
||||
});
|
||||
|
||||
if (LINEAR_TOOL_NAME_SET.has(linearApiName)) {
|
||||
return (
|
||||
<SharedLinearInspector
|
||||
apiName={linearApiName}
|
||||
args={getMcpInputRecord(args, pluginState) || {}}
|
||||
args={input || {}}
|
||||
identifier={'codex'}
|
||||
isArgumentsStreaming={isArgumentsStreaming}
|
||||
isLoading={isLoading}
|
||||
partialArgs={getMcpInputRecord(partialArgs) || {}}
|
||||
partialArgs={partialInput || {}}
|
||||
pluginState={pluginState}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { getCodexLinearMcpApiName, getMcpInputRecord } from './mcpToolUtils';
|
||||
|
||||
describe('getCodexLinearMcpApiName', () => {
|
||||
it('maps Codex Apps fetch calls to entity-specific Linear APIs', () => {
|
||||
expect(
|
||||
getCodexLinearMcpApiName({ input: { id: 'issue:LOBE-10205' }, toolName: 'linear_fetch' }),
|
||||
).toBe('get_issue');
|
||||
expect(
|
||||
getCodexLinearMcpApiName({ input: { id: 'project:Desktop' }, toolName: 'linear_fetch' }),
|
||||
).toBe('get_project');
|
||||
expect(
|
||||
getCodexLinearMcpApiName({ input: { id: 'initiative:AI' }, toolName: 'linear_fetch' }),
|
||||
).toBe('get_initiative');
|
||||
expect(
|
||||
getCodexLinearMcpApiName({
|
||||
input: { id: 'document:agent-runtime' },
|
||||
toolName: 'linear_fetch',
|
||||
}),
|
||||
).toBe('get_document');
|
||||
});
|
||||
|
||||
it('keeps generic Codex Apps Linear search and unknown fetch names renderable', () => {
|
||||
expect(
|
||||
getCodexLinearMcpApiName({
|
||||
input: { query: 'agent runtime' },
|
||||
toolName: 'linear_search',
|
||||
}),
|
||||
).toBe('search');
|
||||
expect(
|
||||
getCodexLinearMcpApiName({ input: { id: 'unknown:123' }, toolName: 'linear_fetch' }),
|
||||
).toBe('fetch');
|
||||
});
|
||||
|
||||
it('normalizes MCP-prefixed and underscored Linear tool names', () => {
|
||||
expect(getCodexLinearMcpApiName({ toolName: '_get_issue' })).toBe('get_issue');
|
||||
expect(getCodexLinearMcpApiName({ toolName: 'linear__get_issue' })).toBe('get_issue');
|
||||
expect(getCodexLinearMcpApiName({ toolName: 'server_linear_get_issue' })).toBe('get_issue');
|
||||
});
|
||||
|
||||
it('treats bare issue identifiers as issue fetch calls', () => {
|
||||
expect(
|
||||
getCodexLinearMcpApiName({ input: { id: 'LOBE-10205' }, toolName: 'linear_fetch' }),
|
||||
).toBe('get_issue');
|
||||
});
|
||||
|
||||
it('does not treat generic fetch or search from other MCP servers as Linear', () => {
|
||||
expect(
|
||||
getCodexLinearMcpApiName({
|
||||
input: { query: 'agent runtime' },
|
||||
server: 'node_repl',
|
||||
toolName: 'search',
|
||||
}),
|
||||
).toBe('');
|
||||
expect(
|
||||
getCodexLinearMcpApiName({
|
||||
input: { id: 'LOBE-10205' },
|
||||
server: 'github',
|
||||
toolName: 'fetch',
|
||||
}),
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
it('allows bare fetch only when the input has a Linear entity prefix', () => {
|
||||
expect(
|
||||
getCodexLinearMcpApiName({
|
||||
input: { id: 'issue:LOBE-10205' },
|
||||
server: 'node_repl',
|
||||
toolName: 'fetch',
|
||||
}),
|
||||
).toBe('get_issue');
|
||||
});
|
||||
|
||||
it('allows generic fetch or search from a Linear server', () => {
|
||||
expect(
|
||||
getCodexLinearMcpApiName({
|
||||
input: { query: 'agent runtime' },
|
||||
server: 'mcp__codex_apps__linear',
|
||||
toolName: 'search',
|
||||
}),
|
||||
).toBe('search');
|
||||
expect(
|
||||
getCodexLinearMcpApiName({
|
||||
input: { id: 'unknown:123' },
|
||||
server: 'mcp__codex_apps__linear',
|
||||
toolName: 'fetch',
|
||||
}),
|
||||
).toBe('fetch');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMcpInputRecord', () => {
|
||||
it('parses JSON string MCP arguments', () => {
|
||||
expect(getMcpInputRecord({ arguments: '{"id":"issue:LOBE-10205"}' })).toEqual({
|
||||
id: 'issue:LOBE-10205',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,12 @@ export interface FormattedMcpValue {
|
||||
|
||||
const LINEAR_CODEX_PREFIX = 'linear_';
|
||||
const LINEAR_CODEX_SERVER_PREFIX = 'server_';
|
||||
const CODEX_LINEAR_FETCH_API_BY_ENTITY: Record<string, string> = {
|
||||
document: 'get_document',
|
||||
initiative: 'get_initiative',
|
||||
issue: 'get_issue',
|
||||
project: 'get_project',
|
||||
};
|
||||
const SERVER_KEYS = ['server', 'serverName', 'server_name', 'connector', 'connector_id'];
|
||||
const TOOL_KEYS = ['tool', 'toolName', 'tool_name', 'name'];
|
||||
const INPUT_KEYS = ['arguments', 'args', 'input', 'params', 'parameters'];
|
||||
@@ -83,17 +89,72 @@ export const getMcpInputRecord = (
|
||||
}
|
||||
};
|
||||
|
||||
export const getCodexLinearMcpApiName = (toolName: string) => {
|
||||
if (!toolName) return '';
|
||||
const normalizeCodexLinearToolName = (toolName: string) => {
|
||||
if (!toolName) return { apiName: '', hasLinearPrefix: false };
|
||||
|
||||
let apiName = toolName.trim();
|
||||
if (apiName.startsWith(LINEAR_CODEX_PREFIX)) {
|
||||
apiName = apiName.slice(LINEAR_CODEX_PREFIX.length);
|
||||
}
|
||||
if (apiName.startsWith(LINEAR_CODEX_SERVER_PREFIX)) {
|
||||
apiName = apiName.slice(LINEAR_CODEX_SERVER_PREFIX.length);
|
||||
let hasLinearPrefix = false;
|
||||
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
|
||||
if (apiName.startsWith(LINEAR_CODEX_PREFIX)) {
|
||||
apiName = apiName.slice(LINEAR_CODEX_PREFIX.length);
|
||||
hasLinearPrefix = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (apiName.startsWith(LINEAR_CODEX_SERVER_PREFIX)) {
|
||||
apiName = apiName.slice(LINEAR_CODEX_SERVER_PREFIX.length);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
while (apiName.startsWith('_')) {
|
||||
apiName = apiName.slice(1);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { apiName, hasLinearPrefix };
|
||||
};
|
||||
|
||||
const isLinearServerName = (server?: string) =>
|
||||
normalizeString(server)
|
||||
.split(/[^a-z0-9]+/iu)
|
||||
.some((part) => part.toLowerCase() === 'linear');
|
||||
|
||||
const getCodexLinearFetchApiName = (
|
||||
input: Record<string, unknown> | undefined,
|
||||
isLinearContext: boolean,
|
||||
) => {
|
||||
const id = normalizeString(input?.id);
|
||||
const entityPrefix = id.includes(':') ? id.slice(0, id.indexOf(':')).toLowerCase() : '';
|
||||
const prefixedApiName = CODEX_LINEAR_FETCH_API_BY_ENTITY[entityPrefix];
|
||||
if (prefixedApiName) return prefixedApiName;
|
||||
|
||||
if (!isLinearContext) return '';
|
||||
|
||||
if (/^[A-Z][A-Z0-9]+-\d+$/u.test(id)) return 'get_issue';
|
||||
|
||||
return 'fetch';
|
||||
};
|
||||
|
||||
export const getCodexLinearMcpApiName = ({
|
||||
input,
|
||||
server,
|
||||
toolName,
|
||||
}: {
|
||||
input?: Record<string, unknown>;
|
||||
server?: string;
|
||||
toolName: string;
|
||||
}) => {
|
||||
const { apiName, hasLinearPrefix } = normalizeCodexLinearToolName(toolName);
|
||||
const isLinearContext = hasLinearPrefix || isLinearServerName(server);
|
||||
|
||||
if (apiName === 'fetch') return getCodexLinearFetchApiName(input, isLinearContext);
|
||||
if (apiName === 'search') return isLinearContext ? apiName : '';
|
||||
|
||||
return apiName;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user