Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f1511b2
feat(core): McpServerConfigSchema を追加 (Atlassian MCP 連携の基盤、Basic/Bear…
shomatan Apr 24, 2026
098983c
feat(core): McpServerConfigSchema に hardening 3 件 (https 強制 / id char…
shomatan Apr 27, 2026
42a5f47
feat(core): ProjectSchema/ProjectMetaSchema に mcpServers[] を追加 (defau…
shomatan Apr 27, 2026
fb930a9
refactor(core): Project 型を z.infer 復帰、既存 fixture に mcpServers: [] を追加
shomatan Apr 27, 2026
f0af787
feat(core): ChatBlock.tool_use に source 追加、RequirementNode に sourceUr…
shomatan Apr 27, 2026
ef98416
feat(core): RequirementNode.sourceUrl を https-only に hardening (Task …
shomatan Apr 27, 2026
07a6aff
feat(ai-engine): redactMcpSecrets utility を追加 (Authorization header の…
shomatan Apr 27, 2026
752bb7f
feat(ai-engine): redactMcpSecrets に case-sensitive 注記と配列値 test を追加
shomatan Apr 27, 2026
90155bc
feat(ai-engine): buildMcpServers utility を追加 (Basic/Bearer auth + all…
shomatan Apr 27, 2026
9121e5d
feat(ai-engine): duplicate-guards 骨格 (interface + strategy dispatcher…
shomatan Apr 27, 2026
6c5c88a
feat(ai-engine): coderef 重複ガードを duplicate-guards/coderef.ts に分離
shomatan Apr 27, 2026
79539a0
feat(ai-engine): question 重複ガードを duplicate-guards/question.ts に分離 (an…
shomatan Apr 27, 2026
2fd94ab
feat(ai-engine): sourceUrl ベース重複ガードを追加 (T1 fix: chat anchor 無しでも動く)
shomatan Apr 27, 2026
3c81d19
refactor(ai-engine): create-node を duplicate-guards に委譲、sourceUrl gua…
shomatan Apr 27, 2026
868ebdb
feat(ai-engine): ChatRunner が buildMcpServers で外部 MCP を合成 (Task 11)
shomatan Apr 27, 2026
1240a45
feat(ai-engine): 外部 MCP の tool_use/tool_result を source=external で永続化…
shomatan Apr 27, 2026
66d5497
feat(ai-engine): tool_result output を永続化時 4KB に truncate (Task 13)
shomatan Apr 27, 2026
312c88d
feat(ai-engine): buildChatPrompt が tool_use/tool_result も replay (Tas…
shomatan Apr 27, 2026
a4a9201
feat(ai-engine): agent-runner refactor + buildMcpServers 共有 (Task 15)
shomatan Apr 27, 2026
86886f3
feat(core,frontend): Project API で mcpServers[] の round-trip を実装 (Tas…
shomatan Apr 27, 2026
1edf5f2
feat(frontend): プロジェクト設定 dialog に MCP サーバー CRUD UI を追加 (Task 17)
shomatan Apr 27, 2026
f6fa0de
feat(frontend): Chat UI で外部 MCP の tool_use を折り畳み表示 (Task 18)
shomatan Apr 27, 2026
51b879b
fix(frontend): MCP サーバー addMcpServer の id 採番衝突を回避
shomatan Apr 28, 2026
101c23a
test(ai-engine): source-url.test.ts の不要な __resetGuardsForTest を削除
shomatan Apr 28, 2026
33aa1c6
fix(core): mcpServers[] の id 重複を superRefine で検出する
shomatan Apr 29, 2026
e4da705
fix(ai-engine): 空 assistant message の永続化を MCP 構築成功後に移動
shomatan Apr 29, 2026
cbe718d
fix(ai-engine): buildChatPrompt の tool_result.output を XML エスケープ
shomatan Apr 29, 2026
dc10dcc
fix(ai-engine): tool_use/属性も XML エスケープし、コメントの誤りを訂正
shomatan Apr 30, 2026
40dfc00
fix(core): MCP server URL の userinfo (user:pass@) を拒否
shomatan Apr 30, 2026
8ae27e9
fix(frontend): MCP サーバー一覧の React key を不変 _uid に分離
shomatan Apr 30, 2026
324b8a6
test(frontend): CR 指摘のテスト精度向上 (mcp 置換 / details 中身 / secret UI 前提)
shomatan Apr 30, 2026
13da60c
fix(ai-engine): tool_result の external 誤分類防止 + text 本文も XML エスケープ
shomatan Apr 30, 2026
43e0d72
test(frontend): 「mcpServers 全消去」テストの seed PATCH に status assert
shomatan Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 149 additions & 3 deletions packages/ai-engine/src/agent-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ describe('runAgent', () => {
id: 'proj-test',
name: 'FRC integration',
codebases: [{ id: 'main', label: 'Main', path: codebaseDir }],
mcpServers: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
Expand Down Expand Up @@ -288,6 +289,7 @@ describe('runAgent', () => {
id: 'proj-test',
name: 'P',
codebases: [{ id: 'main', label: 'Main', path: codebaseDir }],
mcpServers: [],
createdAt: '2026-04-18T00:00:00Z',
updatedAt: '2026-04-18T00:00:00Z',
});
Expand Down Expand Up @@ -436,7 +438,9 @@ describe('runAgent', () => {
it('ingest-document: anchor 無しで起動し、tool_use を素通しする', async () => {
const store = {
getNode: vi.fn(),
getProjectMeta: vi.fn(),
// Task 15: agent-runner は mcpServers[] を取得するため毎ターン getProjectMeta を呼ぶ。
// 空 (mcpServers なし) を返して既存挙動と同等にする。
getProjectMeta: vi.fn().mockResolvedValue(null),
addNode: vi.fn(),
listNodes: vi.fn().mockResolvedValue([]),
findRelatedNodes: vi.fn().mockResolvedValue([]),
Expand Down Expand Up @@ -489,8 +493,150 @@ describe('runAgent', () => {
expect(events.some((e) => e.type === 'error')).toBe(false);
const toolUseEvents = events.filter((e) => e.type === 'tool_use');
expect(toolUseEvents.length).toBeGreaterThan(0);
// anchor 無しなので store.getNode / getProjectMeta は呼ばれない
// anchor 無しなので store.getNode は呼ばれない
expect(store.getNode).not.toHaveBeenCalled();
expect(store.getProjectMeta).not.toHaveBeenCalled();
// Task 15: getProjectMeta は mcpServers[] を取るため必ず呼ばれる
expect(store.getProjectMeta).toHaveBeenCalled();
});

describe('Task 15: agent-runner で buildMcpServers を共有', () => {
const ORIGINAL_ENV = { ...process.env };
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});

it('プロジェクト mcpServers[] を sdk.query に動的に渡す (chat-runner と同じ utility 共有)', async () => {
process.env.TEST_PAT = 'secret';
const store = {
getNode: vi.fn().mockResolvedValue({
id: 'uc-1',
type: 'usecase',
x: 0,
y: 0,
title: 'UC',
body: '',
}),
getProjectMeta: vi.fn().mockResolvedValue({
id: 'p',
name: 'P',
codebases: [],
mcpServers: [
{
id: 'atlassian',
name: 'A',
kind: 'atlassian',
url: 'https://t.test/mcp',
auth: { type: 'pat', scheme: 'bearer', tokenEnvVar: 'TEST_PAT' },
options: { maxChildIssues: 30, maxCommentsPerIssue: 5 },
},
],
createdAt: '2026-04-24T00:00:00Z',
updatedAt: '2026-04-24T00:00:00Z',
}),
addNode: vi.fn(),
listNodes: vi.fn().mockResolvedValue([]),
findRelatedNodes: vi.fn().mockResolvedValue([]),
addEdge: vi.fn(),
} as unknown as ProjectStore;

const querySpy = vi.fn(() =>
(async function* () {
yield {
type: 'result',
subtype: 'success',
result: 'ok',
} as unknown as SdkMessageLike;
})(),
);
const sdk: SdkLike = { query: querySpy };

for await (const _ of runAgent({
sdk,
store,
projectDir: '/ws',
req: {
type: 'start',
agent: 'extract-questions',
projectId: 'p',
input: { nodeId: 'uc-1' },
},
})) {
/* drain */
}

const callArg = (querySpy.mock.calls as unknown[][])[0]?.[0] as unknown as {
options?: {
mcpServers?: Record<string, { headers?: Record<string, string> }>;
allowedTools?: string[];
};
};
expect(Object.keys(callArg.options?.mcpServers ?? {})).toEqual(
expect.arrayContaining(['tally', 'atlassian']),
);
const atlassian = callArg.options?.mcpServers?.atlassian;
expect(atlassian?.headers?.Authorization).toBe('Bearer secret');
// agent 固有の allowedTools (mcp__tally__find_related 等) + 外部 MCP wildcard
expect(callArg.options?.allowedTools).toContain('mcp__atlassian__*');
});

it('env 未設定なら error event を emit、sdk.query は呼ばない', async () => {
delete process.env.MISSING_PAT;
const store = {
getNode: vi.fn().mockResolvedValue({
id: 'uc-1',
type: 'usecase',
x: 0,
y: 0,
title: 'UC',
body: '',
}),
getProjectMeta: vi.fn().mockResolvedValue({
id: 'p',
name: 'P',
codebases: [],
mcpServers: [
{
id: 'a',
name: 'A',
kind: 'atlassian',
url: 'https://t.test/mcp',
auth: { type: 'pat', scheme: 'bearer', tokenEnvVar: 'MISSING_PAT' },
options: { maxChildIssues: 30, maxCommentsPerIssue: 5 },
},
],
createdAt: '2026-04-24T00:00:00Z',
updatedAt: '2026-04-24T00:00:00Z',
}),
addNode: vi.fn(),
listNodes: vi.fn().mockResolvedValue([]),
findRelatedNodes: vi.fn().mockResolvedValue([]),
addEdge: vi.fn(),
} as unknown as ProjectStore;

const querySpy = vi.fn();
const sdk: SdkLike = { query: querySpy };
const events: AgentEvent[] = [];
for await (const e of runAgent({
sdk,
store,
projectDir: '/ws',
req: {
type: 'start',
agent: 'extract-questions',
projectId: 'p',
input: { nodeId: 'uc-1' },
},
})) {
events.push(e);
}

// buildMcpServers の throw が catch (err) で error event に流れる
expect(querySpy).not.toHaveBeenCalled();
const errorEvent = events.find((e) => e.type === 'error');
expect(errorEvent).toBeDefined();
if (errorEvent && errorEvent.type === 'error') {
expect(errorEvent.message).toMatch(/MISSING_PAT/);
}
});
});
});
24 changes: 21 additions & 3 deletions packages/ai-engine/src/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AgentName } from '@tally/core';
import type { ProjectStore } from '@tally/storage';

import { AGENT_REGISTRY } from './agents/registry';
import { buildMcpServers } from './mcp/build-mcp-servers';
import type { AgentEvent, SdkMessageLike } from './stream';
import { sdkMessageToAgentEvent } from './stream';
import { buildTallyMcpServer } from './tools';
Expand Down Expand Up @@ -104,19 +105,36 @@ export async function* runAgent(deps: RunAgentDeps): AsyncGenerator<AgentEvent>
input: parsed.data,
});
try {
// Task 15: プロジェクト設定の mcpServers[] を毎ターン読み込み、buildMcpServers で
// Tally MCP と外部 MCP (Atlassian 等) を合成する。chat-runner と同じ utility を共有。
// env 未設定時は throw → catch で error event に流す。
const projectMeta = await store.getProjectMeta();
const externalConfigs = projectMeta?.mcpServers ?? [];
const { mcpServers, allowedTools: externalAllowed } = buildMcpServers({
tallyMcp: mcp,
configs: externalConfigs,
});

// built-in ツールは mcp__ プレフィックスを持たないもの (Read / Glob / Grep など)。
// options.tools = 実質的な built-in 使用可能リスト。[] を渡せば Bash/Edit/Write 等すべてオフ。
const builtInTools = def.allowedTools.filter((t) => !t.startsWith('mcp__'));
// agent 固有の allowedTools (Tally MCP の具体 tool 名 + built-in) に、外部 MCP の wildcard
// (mcp__<id>__*) を合流。tally の wildcard は agent 側に既に具体名で並んでいるので除外して dedup。
const finalAllowedTools = [
...def.allowedTools,
...externalAllowed.filter((t) => t !== 'mcp__tally__*'),
];

const iter = sdk.query({
prompt: prompt.userPrompt,
options: {
systemPrompt: prompt.systemPrompt,
mcpServers: { tally: mcp as unknown as Record<string, unknown> },
mcpServers,
// built-in ツールは registry で宣言した範囲のみ許可。
// これで find-related-code に Bash / Edit / Write 等が使われなくなる。
tools: builtInTools,
// MCP ツール (mcp__tally__*) も含めて自動承認する
allowedTools: def.allowedTools,
// MCP ツール (mcp__tally__* + 外部 MCP wildcard) を自動承認する
allowedTools: finalAllowedTools,
// 承認リスト外は拒否。built-in 側は tools で絞っているので二重ガード。
permissionMode: 'dontAsk',
// cwd は find-related-code のコード探索スコープ。未指定エージェントは SDK デフォルト。
Expand Down
4 changes: 4 additions & 0 deletions packages/ai-engine/src/agents/find-related-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('findRelatedCodeAgent.validateInput', () => {
id: 'proj-frc',
name: 'FRC',
codebases: [{ id: 'main', label: 'Main', path: codebaseDir }],
mcpServers: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
Expand Down Expand Up @@ -106,6 +107,7 @@ describe('findRelatedCodeAgent.validateInput', () => {
id: 'proj-frc',
name: 'FRC',
codebases: [],
mcpServers: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
Expand All @@ -122,6 +124,7 @@ describe('findRelatedCodeAgent.validateInput', () => {
id: 'proj-frc',
name: 'FRC',
codebases: [{ id: 'main', label: 'Main', path: filePath }],
mcpServers: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
Expand All @@ -136,6 +139,7 @@ describe('findRelatedCodeAgent.validateInput', () => {
id: 'proj-frc',
name: 'FRC',
codebases: [{ id: 'main', label: 'Main', path: '../nonexistent-xyz' }],
mcpServers: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
Expand Down
Loading