diff --git a/CHANGELOG.md b/CHANGELOG.md index a4222b7..b5ea7f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.12.5 + +- Handle the real Serena 1.7/FastMCP structuredContent.result string envelope before parsing symbols or references. Preserve error, empty-result, malformed and shortened-response semantics; do not discard unknown envelope metadata or silently prefer text over unsupported structured data. +- Add an opt-in real Serena acceptance command using an explicitly supplied installed command. It exercises overloaded C# identities, ambiguity, reference locations, direct upstream body evidence, legitimate empty results, process interruption with unavailable restart, inactive projects and owned-process cleanup. It neither installs dependencies nor reconnects the parent Codex client. + ## 0.12.4 - Repomix health and packing launch an installed JavaScript bin directly with the current Node executable and separate arguments, eliminating the cmd/npx shell chain. Local package bin discovery and explicit absolute customCliPath are supported; shell wrappers and npx caches are no longer invoked. Missing or invalid entries use the builtin packer without installing anything. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80b84d1..e92ad9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,6 +13,8 @@ npm run delivery:verify Reports and bounded stage logs are under `test-tmp/check//`. CI uploads only the compact report, including failures; it does not upload local workspaces or screenshots. A passing core check does not establish desktop or real-upstream acceptance. +`npm run test:serena-real -- [launcher arguments]` performs an opt-in real C# upstream acceptance in a generated `test-tmp/serena-acceptance/` fixture. Supply a command that accepts Serena CLI arguments and an isolated SERENA_HOME launcher; configure/download its C# language server only within an authorized setup. The script uses normal production adapter timeouts, records result/error/cleanup and PID exit, and never installs prerequisites. Body retrieval is a direct upstream oracle, not a claim that WinCode returns whole method bodies. Native reference coordinates may identify the containing class rather than the exact call line. This probe does not register or reload the user's Codex MCP connection. + For opt-in fixed-profile TavernDesk acceptance, use `npm run test:tavern-context -- --ui-pid= --ui-hwnd=` and `npm run test:product -- `. The latter checks the fixed profile receipt and performs six navigation-to-source tasks without source filenames supplied in advance. Native candidate discovery is counted, source reads used only as the oracle are separate, and all returned bodies are checked against current file hashes. Source candidates remain distinct from verified runtime bindings. The scripts do not install Serena, activate a language server, launch the target application or use personal databases. The delivery manifest covers Gateway JavaScript, all published Host files including dependency sidecars, four managed Skill documents, and package/SDK/Host lock configuration. It records the Git revision and toolchains. Timestamps and checkout paths do not participate in content identity. Hashes detect local mismatches; they are not signatures. Run a complete check after changing delivery inputs. Keep a complete previous checkout/artifact set for rollback; do not mix old DLLs with a new Gateway. diff --git a/README.md b/README.md index 79ae25e..a1a3399 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ WinCode is a local MCP server built for Windows and .NET engineering. It bridges - **Inspect the running app:** Enumerate visible windows, query specific controls or subtrees, and capture numbered visual overlays without activating or stealing focus from the target. - **Review with evidence:** Trace on-screen widgets back to literal XAML declaration tags, line numbers, and file hashes, with transparent reporting for ambiguity, truncation, or degraded upstreams. -Current source version: **0.12.4**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. +Current source version: **0.12.5**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. ### Quick start @@ -211,7 +211,7 @@ WinCode 是面向 Windows 与 .NET 工程研发的本地 MCP 服务。它将项 - **观察实际界面:**发现系统可见窗口,按条件定向查询目标控件或子树,并在不激活、不抢占前台焦点的前提下获取数字标注截图。 - **源码双向印证:**将运行时抓取的控件关联回 XAML 源码声明的起始行号、代码片段与文件哈希,清晰报告歧义、截断与降级状态。 -当前源码版本为 **0.12.4**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 +当前源码版本为 **0.12.5**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 ### 快速上手 diff --git "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" index 34ab85b..767c706 100644 --- "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" +++ "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" @@ -238,3 +238,4 @@ ToolRouter 是允许知道具体实现的组装位置;Gateway 不再访问适 - 2026-09-08 20:44:51(北京时间):WP4 / 0.12.2 精确 head 的 Node 22/24 与 CodeQL 全通过,合并 [PR #25](https://github.com/linnnn89/WinCode/pull/25),main=ba94c00。D3 仍未决,工程交付完成不等于分支保护已生效。 - WP5 / 0.12.3:已完成新 stdio 精准范围、固定 TavernDesk 六导航→源码候选、真实 WPF 修复闭环和实际截图核对;产品任务 30 次调用/67103 UTF-16 字符,127 显示源码行中 42 行重复,不宣称普遍提速。实际 Codex hello 仍为 0.11.2,安装 Skill 已备份同步。真实 Serena 命令缺失、用户新导入角色卡尚待定位,按本计划保持部分完成;详见工作日志验收矩阵。F12/D3 未决状态不变。 - WP5 新卡补充:用户指明书架角色后,入库、原始 JSON 字段保持、头像文件与书架截图已核对;在新 0.12.3 Gateway/Host 中,同名两文本节点正确返回完整但 ambiguous,不猜测唯一项。含真实卡的六导航取证再度 6/6;详情动作/聊天、真实 Serena、当前 Codex 重连仍未验收。不会把 Computer Use 误归属问题记为 WinCode 故障。 +- 2026-09-08 21:43(北京时间)补充:用户同意尝试四项检查后,D3 main 保护已启用并回读,F12 经 PR #27 合并后 CodeQL alert #1 自动 fixed。用户另行批准隔离安装 Serena(1 GB / 15 分钟上限),真实 1.7.0/Roslyn 链暴露并定位 FastMCP result 包装兼容缺陷;0.12.5 修复及七项真实验收已进入最终回归。正文证据为直接上游 oracle,WinCode 符号/引用为真实 adapter;父 Codex 重连仍未完成,详见工作日志。 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index a284fcb..d3ec759 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -495,3 +495,13 @@ - Serena 固定 v1.7.0 commit=949a27ef1e5fda1a6e7b561e777bcece345c6ffd,复用现有 Python 3.13.7,venv/uv-cache/SERENA_HOME/语言服务位于 test-tmp/serena-real;安装完成约 632 MB,.NET 10 复用已有环境。C# Roslyn=5.5.0-2.26078.4,下载校验采用上游固定 SHA-256。握手真实成功,第一次查询发现 structuredContent.result 包裹 JSON 字符串被当成符号对象;上游 content 内实际返回三个完整身份。此新缺陷作为后续兼容修复,不把本 PR 写成 Serena 已验收。 - 当前 Codex 管理 CLI 只有 list/get/add/remove/login/logout,没有受支持的 reconnect 命令。保存配置指向正确 I:/WinCode/dist/index.js;没有用重复注册或杀进程替代重连,也未宣称旧父连接更新。 - 作者反证自审:无 shell 不等于已安装脚本可信或有沙盒;缺失显式 CLI 不应悄悄执行另一安装。CodeQL 任务成功仍需合并后读取 alert #1 的 fixed 状态;PR/远端验收待后续回执。 + +## 2026-09-08 21:43 — 0.12.5 真实 Serena 兼容闭环 + +- 0.12.4 PR #27 精确 head=b912343 的 Node 22/24 和三语言 CodeQL 均通过,2026-09-08 21:40:05 在启用保护后正常 squash 合并,main=41602e0af4a1738f2542ae513eec1dda1485df29。未使用 admin bypass。main 扫描后 alert #1 已自动 fixed,fixed_at=2026-09-08T13:41:50Z;未手动 dismiss。 +- 真实 Serena 原始返回同时包含 content JSON 和 structuredContent={result:JSON字符串}。旧实现优先序列化整个 structuredContent,误判为不支持的符号结构。现在仅解开唯一 result 字符串包装,其余结构仍执行原有校验;不吞掉未知元数据,不将畸形 structuredContent 的 text 作为成功替代。 +- 新增六项回归与既有身份/降级套件合计 52/52:真实包装的符号/引用、合法空数组、未激活错误、截断、非 JSON、额外 metadata。引用坐标仍明确 containing-symbol,不伪装为精确调用行。 +- 新增显式 test:serena-real,不安装组件、不修改 Codex 注册。复用已批准隔离安装的 Serena 1.7.0 与 Roslyn,按生产默认连接/调用超时执行专用 C# 项目;七项通过:三个同名/重载身份、歧义不猜测、两个重载分别命中各自调用、直接上游正文与源码逐字一致、合法空符号/零引用、终止自有上游后将测试重启命令设为缺失并确认降级、真实未激活项目。保留 direct upstream body oracle 和 WinCode adapter 查询的证据边界,不宣称新增全方法正文产品接口。 +- 报告 test-tmp/serena-acceptance/1788874762075-31176/report.json 与 1788874881144-38796/report.json;每次记录真实结果、PID 退出、dispose 成败,测试 C# 源码保持一致。进一步自审加强为核对引用预览中 > 标记所在行,避免“周围文本同时有另一重载”造成假阳性;最终复测回执另存本地。查询已隔离安装目录对应 python/dotnet 进程,没有残留匹配进程。Serena 环境最终约 632 MB,复用 Python 3.13.7,无全局 PATH 修改、无全局 Serena 启用。 +- 首轮 0.12.5 全量回归和 stdio 通过,交付阶段因检查运行期间补充 package.json 维护命令触发源码/构建指纹不一致而失败;这是有效的一致性保护。冻结变更后完整重跑,不绕过交付校验。失败报告 2026-09-08T13-40-56-234Z-core。 +- 当前 Codex 旧连接仍需客户端重连;没有提供可调用的重连接口,不用结束 Codex/强杀 Gateway 冒充成功。作者自审,未新增独立审查或子代理。 diff --git a/package-lock.json b/package-lock.json index 9c0cfb3..ea00e49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wincode-mcp", - "version": "0.12.4", + "version": "0.12.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wincode-mcp", - "version": "0.12.4", + "version": "0.12.5", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", diff --git a/package.json b/package.json index 27cd3e4..8c5e84e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wincode-mcp", - "version": "0.12.4", + "version": "0.12.5", "description": "Windows-first MCP gateway: .NET project graph, evidence-bounded context, honest change-impact, long-running process hygiene", "main": "dist/index.js", "type": "module", @@ -17,6 +17,7 @@ "benchmark:agent": "tsx scripts/benchmark-agent-efficiency.ts", "test:benchmark": "tsx --test tests/agent-efficiency-benchmark.test.ts", "test:product": "tsx scripts/verify-product-tasks.ts", + "test:serena-real": "tsx scripts/verify-serena-real.ts", "test:tavern-context": "tsx scripts/verify-tavern-context.ts", "test:e2e": "tsx scripts/test-mcp-client.ts", "typecheck": "tsc -p tsconfig.test.json", diff --git a/scripts/verify-serena-real.ts b/scripts/verify-serena-real.ts new file mode 100644 index 0000000..ef0e9bb --- /dev/null +++ b/scripts/verify-serena-real.ts @@ -0,0 +1,135 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { SerenaAdapter } from '../src/Adapters/SerenaAdapter.js'; +import { CacheManager } from '../src/Core/Cache.js'; +import { getDefaultConfig } from '../src/Core/Config.js'; +import { killProcessTree } from '../src/Core/ResourceManager.js'; + +// Explicit opt-in: use an already installed, isolated Serena command. Never install prerequisites here. +const [command, ...prefixArgs] = process.argv.slice(2); +if (!command || !path.isAbsolute(command)) throw new Error('Usage: test:serena-real -- [launcher arguments]. The command must accept Serena CLI arguments.'); +await fs.access(command); +const root = path.resolve('test-tmp/serena-acceptance', `${Date.now()}-${process.pid}`); +await fs.mkdir(path.join(root, '.serena'), { recursive: true }); +await fs.copyFile('global.json', path.join(root, 'global.json')); +await fs.writeFile(path.join(root, '.serena/project.yml'), 'project_name: wincode-real-acceptance\nlanguage_servers: [csharp]\nread_only: true\n'); +await fs.writeFile(path.join(root, 'Fixture.csproj'), 'net10.0'); +const source = `namespace Fixture; +public class Service { + public int Save(int value) { return value + 1; } + public string Save(string value) { return value + "!"; } + public int Unused() { return 42; } +} +public class Other { + public int Save(int value) { return value - 1; } +} +public class Caller { + public int Run() { return new Service().Save(7); } + public string Text() { return new Service().Save("y"); } +} +`; +await fs.writeFile(path.join(root, 'Service.cs'), source); +const report: any = { startedAt: new Date().toISOString(), command, prefixArgs, fixture: root, stages: [], passed: false }; +const adapters: SerenaAdapter[] = []; +async function create(active: boolean) { + const config = getDefaultConfig(root); + config.adapters.serena.customCommand = command; + config.adapters.serena.customArgs = [...prefixArgs, 'start-mcp-server', + ...(active ? ['--project', root] : []), '--enable-web-dashboard', 'false', '--open-web-dashboard', 'false', + '--enable-gui-log-window', 'false', '--log-level', 'WARNING']; + const cache = new CacheManager(path.join(root, active ? 'active-cache' : 'inactive-cache')); + await cache.initialize(); + const adapter = new SerenaAdapter(config, cache); + adapters.push(adapter); + await adapter.initialize(); + return { adapter, config }; +} +async function stage(name: string, run: () => Promise) { + const start = Date.now(); + try { const result = await run(); report.stages.push({ name, passed: true, ms: Date.now() - start, result }); } + catch (error) { report.stages.push({ name, passed: false, ms: Date.now() - start, error: String(error) }); throw error; } +} +try { + const { adapter, config } = await create(true); + await stage('same-name and overload identities through WinCode adapter', async () => { + const found = await adapter.findSymbolsDetailed('Save', undefined, 'Service.cs'); + assert.equal(found.source, 'serena-mcp'); assert.equal(found.queryComplete, true); + assert.deepEqual(found.symbols.map(s => [s.namePath, s.line]), [ + ['Fixture/Service/Save[0]', 3], ['Fixture/Service/Save[1]', 4], ['Fixture/Other/Save', 8], + ]); + assert.equal(adapter.getUpstreamStatus().semanticQueryUsable, true); + return { found, health: adapter.getUpstreamStatus() }; + }); + await stage('ambiguous name never selects an overload', async () => { + const refs = await adapter.findReferencesDetailed('Save', 'Service.cs'); + assert.equal(refs.resolution, 'ambiguous'); assert.equal(refs.candidateCount, 3); + assert.equal(refs.queryComplete, false); return refs; + }); + await stage('explicit overload references retain containing-symbol coordinates', async () => { + const results = []; + for (const [name, preview, zeroReferenceLine] of [ + ['Fixture/Service/Save[0]', 'Save(7)', 10], ['Fixture/Service/Save[1]', 'Save("y")', 11], + ] as const) { + const refs = await adapter.findReferencesDetailed(name, 'Service.cs'); + assert.equal(refs.source, 'serena-mcp'); assert.equal(refs.queryComplete, true); + assert.equal(refs.resolution, 'resolved'); assert.equal(refs.totalReferences, 1); + assert.equal(refs.target?.namePath, name); assert.ok(refs.references[0].preview.includes(preview)); + const marked = refs.references[0].preview.split('\n').find(line => /^\s*>\s*\d+:/.test(line)); + assert.ok(marked?.includes(`> ${zeroReferenceLine}:`) && marked.includes(preview), 'marked reference, not merely surrounding context, matches this overload'); + assert.equal(refs.references[0].lineKind, 'containing-symbol'); + assert.equal(refs.references[0].line, 10); results.push(refs); + } + return results; + }); + await stage('real upstream body matches fixture source (direct upstream oracle)', async () => { + // Test-only access to the actual connected upstream; do not add a public product API for this oracle. + const result = await (adapter as any).serenaClient.callTool({ name: 'find_symbol', arguments: { + name_path_pattern: '/Fixture/Service/Save[0]', relative_path: 'Service.cs', include_body: true, + } }); + assert.notEqual(result.isError, true); + const body = JSON.parse(result.content[0].text); + assert.equal(body.length, 1); assert.equal(body[0].body, source.split('\n')[2].trim()); + return result; + }); + await stage('valid empty symbols and references stay semantic', async () => { + const empty = await adapter.findSymbolsDetailed('AbsentSymbol', undefined, 'Service.cs'); + const unused = await adapter.findReferencesDetailed('Fixture/Service/Unused', 'Service.cs'); + assert.equal(empty.source, 'serena-mcp'); assert.equal(empty.queryComplete, true); assert.equal(empty.totalFound, 0); + assert.equal(unused.source, 'serena-mcp'); assert.equal(unused.queryComplete, true); assert.equal(unused.totalReferences, 0); + return { empty, unused }; + }); + await stage('actual process interruption and unavailable restart degrade honestly', async () => { + const pid = (adapter as any).serenaPid as number; + assert.ok(pid > 0); + // Only the test-owned Serena tree is terminated. Its restart command is made unavailable in this fixture. + config.adapters.serena.customCommand = path.join(root, 'missing-serena.exe'); + await killProcessTree({ pid }); + for (let i = 0; i < 100 && adapter.getUpstreamStatus().handshakeOk; i++) await new Promise(r => setTimeout(r, 20)); + const fallback = await adapter.findSymbolsDetailed('Unused', undefined, 'Service.cs'); + assert.equal(fallback.source, 'serena-adapter-fallback'); + assert.equal(adapter.getUpstreamStatus().semanticQueryUsable, false); + assert.throws(() => process.kill(pid, 0), { code: 'ESRCH' }); + return { fallback, health: adapter.getUpstreamStatus(), pidExited: pid }; + }); + await stage('real unactivated project remains inactive', async () => { + const { adapter: inactive } = await create(false); + const found = await inactive.findSymbolsDetailed('Save', undefined, 'Service.cs'); + assert.equal(found.source, 'serena-adapter-fallback'); assert.equal(found.queryComplete, false); + assert.equal(inactive.getUpstreamStatus().projectActive, false); + return { found, health: inactive.getUpstreamStatus() }; + }); + assert.equal(await fs.readFile(path.join(root, 'Service.cs'), 'utf8'), source); + report.passed = true; +} catch (error) { report.error = String(error); process.exitCode = 1; } +finally { + const pids = adapters.map(a => (a as any).serenaPid as number | null).filter((p): p is number => Boolean(p)); + const cleanup = await Promise.allSettled(adapters.map(a => a.dispose())); + report.cleanup = cleanup.map(r => r.status === 'fulfilled' ? { closed: true } : { closed: false, error: String(r.reason) }); + if (cleanup.some(r => r.status === 'rejected')) { report.passed = false; process.exitCode = 1; } + report.pidExit = pids.map(pid => { try { process.kill(pid, 0); return { pid, exited: false }; } catch (e: any) { return {pid, exited: e.code === 'ESRCH'}; } }); + if (report.pidExit.some((p: any) => !p.exited)) { report.passed = false; process.exitCode = 1; } + report.completedAt = new Date().toISOString(); + await fs.writeFile(path.join(root, 'report.json'), JSON.stringify(report, null, 2)); + console.log(JSON.stringify({ passed: report.passed, stages: report.stages.map((s: any) => ({ name: s.name, passed: s.passed })), report: path.join(root, 'report.json') })); +} diff --git a/skills/wincode/SKILL.md b/skills/wincode/SKILL.md index 454bb6f..ae5efe6 100644 --- a/skills/wincode/SKILL.md +++ b/skills/wincode/SKILL.md @@ -5,7 +5,7 @@ description: 使用 WinCode MCP 分析 Windows/.NET 工作区,或读取桌面 # WinCode -仓库手册版本:0.12.4。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行,不在每个任务中例行检查。以当前连接实际 Schema 为准,手册版本不证明 MCP 已重连。 +仓库手册版本:0.12.5。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行,不在每个任务中例行检查。以当前连接实际 Schema 为准,手册版本不证明 MCP 已重连。 仅按当前任务读取对应手册,不预读全部文件: - 代码、上下文、引用、影响分析:[code](references/code.md)。 diff --git a/skills/wincode/references/diagnostics.md b/skills/wincode/references/diagnostics.md index bf1f79e..731bf02 100644 --- a/skills/wincode/references/diagnostics.md +++ b/skills/wincode/references/diagnostics.md @@ -1,5 +1,7 @@ # 诊断与审计 +0.12.5 已兼容 Serena 1.7/FastMCP 的 structuredContent.result 字符串包装。真实上游验收入口是维护命令 `npm run test:serena-real -- <已安装命令绝对路径> [启动器参数]`,只在用户要求验收且环境已准备时执行;它创建独立 C# 夹具,记录重载、引用、空结果、未激活、断连降级及 PID 退出。脚本不自动安装,也不把 commandFound/握手成功当作语义可用。安装在 test-tmp 的上游仅用于隔离验收,不表示 Codex 默认连接已启用 Serena。 + 从 0.12.4 起 Repomix 健康探测和打包都由当前 Node 可执行文件直接启动已安装的 JavaScript CLI;不经过 cmd、npx 或 PATH 包装脚本,也不下载包。默认按目标工作区和 WinCode 安装目录的 Node 模块路径读取 repomix/package.json 的 bin 入口;不搜索 npx 缓存或 npm 自定义全局前缀。非标准安装需在宿主 WinCodeConfig.adapters.repomix.customCliPath 提供绝对 .js/.cjs/.mjs 路径;该字段不是 MCP 工具参数,不能传给 hello/prepare_context。显式路径无效时返回 builtin fallback,不执行另一份安装;useCli=false 仍完全禁止探测和启动。执行已安装脚本不提供沙盒或脚本可信性保证。 `hello` 从 0.12.1 起只读取版本、能力和已知状态,不启动上游、CLI 或 UI Host 探测进程。`health.healthObservation` 区分 `known/unknown` 并给出 `observedAt`;`unknown`、`available:null` 或 `commandFound:null` 表示尚未探测,不能解释为不可用。配置禁用属于已知策略,但观察时间可为 null。已知健康结果可能陈旧,需要当前检查时调用现有 `wincode_diagnose_project({})`,不向 hello 添加未声明的 force/probe 字段。 diff --git a/src/Adapters/SerenaAdapter.ts b/src/Adapters/SerenaAdapter.ts index a88e262..80c0ad5 100644 --- a/src/Adapters/SerenaAdapter.ts +++ b/src/Adapters/SerenaAdapter.ts @@ -638,7 +638,13 @@ export class SerenaAdapter implements IAdapter { } private serenaResultText(result: Awaited>): string { - if (result.structuredContent !== undefined) return JSON.stringify(result.structuredContent); + if (result.structuredContent !== undefined) { + const structured = result.structuredContent; + // FastMCP wraps a string tool return as { result: "..." }, including JSON and errors. + if (this.isRecord(structured) && Object.keys(structured).length === 1 && typeof structured.result === 'string') + return structured.result; + return JSON.stringify(structured); + } return Array.isArray(result.content) ? result.content.filter(block => block.type === 'text').map(block => (block as { text: string }).text).join('\n') : ''; diff --git a/src/Core/Config.ts b/src/Core/Config.ts index 12e468b..ddb9d38 100644 --- a/src/Core/Config.ts +++ b/src/Core/Config.ts @@ -1,6 +1,6 @@ import path from 'node:path'; -export const WINCODE_VERSION = '0.12.4'; +export const WINCODE_VERSION = '0.12.5'; /** * Bounded waits for every external process/RPC. None of these may be Infinity. diff --git a/tests/serena-identity.test.ts b/tests/serena-identity.test.ts index 3fe8b19..3cdbf8c 100644 --- a/tests/serena-identity.test.ts +++ b/tests/serena-identity.test.ts @@ -193,6 +193,42 @@ it('structured content is parsed even when there is no text content', async () = assert.equal(found.symbols[0].namePath, 'Service/Save[0]'); }); +it('unwraps the real Serena 1.7 FastMCP string envelope for symbols and references', async () => { + const { adapter } = fixture(); + (adapter as any).serenaClient.callTool = async ({name}: {name:string}) => ({ content: [], structuredContent: { + result: JSON.stringify(name === 'find_symbol' ? [symbol()] : { 'Caller.cs': { Class: [ + { name_path: 'Caller', body_location: {start_line: 9, end_line: 12}, content_around_reference: 'Save(7);' }, + ] } }), + } }); + const found = await adapter.findSymbolsDetailed('Save'); + assert.equal(found.source, 'serena-mcp'); + assert.equal(found.symbols[0].namePath, 'Service/Save[0]'); + const refs = await adapter.findReferencesDetailed('Service/Save[0]', 'src/Service.cs'); + assert.equal(refs.queryComplete, true); + assert.equal(refs.source, 'serena-mcp'); + assert.equal(refs.references[0].line, 10); + assert.equal(refs.references[0].lineKind, 'containing-symbol'); +}); + +for (const value of ['[]', 'Error: No active project.', 'The answer is too long', 'not-json']) { + it(`preserves envelope payload semantics: ${value}`, async () => { + const { adapter } = fixture(); + (adapter as any).serenaClient.callTool = async () => ({ content: [], structuredContent: { result: value } }); + const found = await adapter.findSymbolsDetailed('Save'); + assert.equal(found.queryComplete, value === '[]'); + assert.equal(found.source, value === '[]' ? 'serena-mcp' : 'serena-adapter-fallback'); + if (value === 'Error: No active project.') assert.equal(adapter.getUpstreamStatus().projectActive, false); + }); +} + +it('does not hide extra envelope metadata or turn malformed structured content into text success', async () => { + const { adapter } = fixture(); + (adapter as any).serenaClient.callTool = async () => ({ content: [{type:'text',text:JSON.stringify([symbol()])}], + structuredContent: { result: JSON.stringify([symbol()]), truncated: true } }); + const found = await adapter.findSymbolsDetailed('Save'); + assert.equal(found.queryComplete, false); +}); + for (const complete of [true, false]) { it('ImpactAnalyzer skips reference calls for ' + (complete ? 'ambiguous' : 'incomplete') + ' identity', async () => { const { adapter } = fixture(); diff --git a/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj b/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj index 61bb561..02837c3 100644 --- a/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj +++ b/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj @@ -1,7 +1,7 @@ - 0.12.4 + 0.12.5 true Exe net10.0-windows