Skip to content

Commit 4047f90

Browse files
committed
Clarify MCP result projection guidance and coverage
1 parent ba3bdf5 commit 4047f90

9 files changed

Lines changed: 132 additions & 7 deletions

File tree

SDL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Use this file as the repository fallback for the `sdl-mcp-agent-workflow` skill.
44

55
SDL-MCP is the normal repository interface. Native filesystem and shell tools are fallback-only when SDL-MCP is unavailable, or when accessing agent memory and other internal client data outside the indexed repository.
66

7+
When a client bridge returns the raw MCP result, prefer `structuredContent`; use text `content` only as a fallback for older servers. Do not emit both or return the whole MCP response envelope to the agent.
8+
79
---
810

911
## 1. Start Every Task
@@ -175,7 +177,7 @@ Use this when you need likely files and symbols before choosing `symbol.edit` or
175177

176178
Use the cheapest rung that answers the task. Static price tags in `sdl.manual` and `sdl.action.search` are release-time estimates, not live telemetry; use them to choose the first probe, then use `usage.stats` and `signalDensity` only when you need a savings or waste report.
177179

178-
- Runtime: prefer `outputMode: "digest"` for build/test/lint and other noisy commands. The digest keeps a compact parsed status and persists full output for `runtimeQueryOutput`.
180+
- Runtime: prefer `outputMode: "digest"` for build/test/lint and other noisy commands. The digest keeps a compact parsed status and persists full output for `runtimeQueryOutput`. Do not guess `runtimeQueryOutput` arguments; replay a returned action unchanged or call focused `sdl.manual` for `runtime.queryOutput` first.
179181
```json
180182
{
181183
"fn": "runtimeExecute",

docs/feature-deep-dives/code-mode.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Code Mode keeps those flows inside SDL-MCP:
3838

3939
## Output Surfaces
4040

41-
Code Mode tool output is human-first. The first MCP `content` text block is concise terminal-friendly text, while task-relevant machine-readable data is carried in `structuredContent`. Agents should read the visible text for the human-facing summary and use `structuredContent` for follow-up identifiers such as `etag`, handles, file paths, symbol IDs, references, summaries, errors, and next-action hints.
41+
Raw Code Mode MCP results retain a concise human-readable `content` text block beside task-relevant `structuredContent`. Agent bridges should pass through `structuredContent` when it is present, use text `content` only as a compatibility fallback for older servers, and avoid exposing both or the whole MCP response envelope to the agent. Follow-up identifiers such as `etag`, handles, file paths, symbol IDs, references, summaries, errors, and next-action hints remain in `structuredContent`.
4242

4343
SDL-MCP internal bookkeeping is not duplicated into model-visible output by default. Timing diagnostics, packed-wire stats, raw-context baselines, action traces, precondition snapshots, backup paths, and retrieval-debug details stay in logs or diagnostics surfaces. Set `includeDiagnostics: true` or the relevant retrieval-evidence option only when the task actually needs those details; even then, the normal visible text stays concise.
4444

src/mcp/server-instructions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ export const SDL_MCP_SERVER_INSTRUCTIONS = [
22
"At session start, load and follow `sdl-mcp-agent-workflow` when skills are available; otherwise use these instructions. The index-refresh authorization rules below govern if other guidance conflicts.",
33
"",
44
"Required SDL-MCP workflow:",
5+
"Raw MCP result handling: When a client bridge returns the raw MCP result, prefer `structuredContent`; use text `content` only as a fallback for older servers. Do not emit both or return the whole MCP response envelope to the agent.",
56
'1. Confirm `repo.status` through `sdl.workflow` with `fn: "repoStatus"` and `args: {}`.',
67
"2. If the correct action is unclear, use `sdl.action.search`; load `sdl.manual` only for the focused schema needed to compose a non-obvious request.",
78
'3. Gather task evidence via `sdl.context` with `budget.maxTokens` and flat `focusPaths`, `focusSymbols`, or `chatMentions`; never send `options`, `contextMode`, or `answerFirst`. Use `sdl.retrieve` for one-step `symbolSearch`, `symbolGetCard`, `sliceBuild`, `codeSkeleton`, `codeHotPath`, or bounded `codeNeedWindow` retrieval.',
89
'4. Use `responseMode: "auto"` for potentially large responses. When a result returns a canonical `response.get` continuation (`nextAction` or `action`) for `sdl.retrieve` with `op: "responseGet"`, replay its returned action and arguments unchanged; outer `repoId` owns trusted dispatch, `detail` and `includeDiagnostics` stay outer controls, and nested `args.repoId` is invalid. Use workflow `responseGet` only when direct `sdl.retrieve` is unavailable or an existing multi-step workflow needs it.',
910
'5. Reuse `{ ref, unchanged: true }` responses and session short ids (`sN`); pass `refsMode: "off"` only when full content is required.',
1011
"6. Use `sdl.workflow` for multi-step pipelines, runtime execution, data transforms, batch operations, mutations, or result piping between steps; do not wrap a single `sdl.context` call.",
1112
'7. Use `sdl.file` with `op: "read"`; `file.read` only for non-indexed files and prefer targeted reads. `op: "write"` (`file.write`) supports targeted single-file writes, including indexed files with live reconciliation; prefer `op: "symbolEditPreview"`/`op: "symbolEditApply"` (`symbol.edit` preview/apply) for one-symbol indexed edits and `op: "searchEditPreview"`/`op: "searchEditApply"` for cross-file edits.',
12-
'8. runtimeExecute executes repository tooling. Permitted uses include build, test, lint, compiler, named scripts, and targeted edit scripts. Do not use it to inspect, search, or print repository files. Use `sdl.context` or `sdl.retrieve` for indexed source and `sdl.file` with `op="read"` for other files. For permitted tooling, use workflow `runtimeExecute` with an explicit `timeoutMs`: use `outputMode: "minimal"` for quiet probes, `outputMode: "digest"` for noisy commands, or `outputMode: "intent"` when exact terms are known; set `persistOutput: true` when logs may need focused `runtimeQueryOutput` queries.',
13+
'8. runtimeExecute executes repository tooling. Permitted uses include build, test, lint, compiler, named scripts, and targeted edit scripts. Do not use it to inspect, search, or print repository files. Use `sdl.context` or `sdl.retrieve` for indexed source and `sdl.file` with `op="read"` for other files. For permitted tooling, use workflow `runtimeExecute` with an explicit `timeoutMs`: use `outputMode: "minimal"` for quiet probes, `outputMode: "digest"` for noisy commands, or `outputMode: "intent"` when exact terms are known; set `persistOutput: true` when logs may need focused `runtimeQueryOutput` queries. Do not guess `runtimeQueryOutput` arguments; replay a returned action unchanged or call focused `sdl.manual` for `runtime.queryOutput` first.',
1314
"9. Use memory tools only when `memory.enabled: true`.",
1415
"10. Never call `index.refresh`, directly, through `sdl.workflow`, or via `sdl-mcp index`, without explicit user approval in the current turn.",
1516
'11. `derivedState.structuralStale` and `derivedState.semanticStale` report separate readiness classes. If only `summariesDirty` or `embeddingsDirty` is set, continue with available retrieval lanes; do not refresh the index. These flags, `graphIntegrityState: "verifying"`, `PARSER_FILE_STATE_MISSING`, parser-state warnings, parser-provenance warnings, and refresh recommendations are diagnostics, not approval.',

templates/SDL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Use this file as the repository fallback for the `sdl-mcp-agent-workflow` skill.
44

55
SDL-MCP is the normal repository interface. Native filesystem and shell tools are fallback-only when SDL-MCP is unavailable, or when accessing agent memory and other internal client data outside the indexed repository.
66

7+
When a client bridge returns the raw MCP result, prefer `structuredContent`; use text `content` only as a fallback for older servers. Do not emit both or return the whole MCP response envelope to the agent.
8+
79
---
810

911
## 1. Start Every Task
@@ -175,7 +177,7 @@ Use this when you need likely files and symbols before choosing `symbol.edit` or
175177

176178
Use the cheapest rung that answers the task. Static price tags in `sdl.manual` and `sdl.action.search` are release-time estimates, not live telemetry; use them to choose the first probe, then use `usage.stats` and `signalDensity` only when you need a savings or waste report.
177179

178-
- Runtime: prefer `outputMode: "digest"` for build/test/lint and other noisy commands. The digest keeps a compact parsed status and persists full output for `runtimeQueryOutput`.
180+
- Runtime: prefer `outputMode: "digest"` for build/test/lint and other noisy commands. The digest keeps a compact parsed status and persists full output for `runtimeQueryOutput`. Do not guess `runtimeQueryOutput` arguments; replay a returned action unchanged or call focused `sdl.manual` for `runtime.queryOutput` first.
179181
```json
180182
{
181183
"fn": "runtimeExecute",

templates/sdl-mcp-agent-workflow/SKILL.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ description: Use when working in an SDL-MCP-enabled repository, including reposi
77

88
Use SDL-MCP as the repository boundary.
99

10+
When a client bridge returns the raw MCP result, prefer `structuredContent`; use
11+
text `content` only as a fallback for older servers. Do not emit both or return
12+
the whole MCP response envelope to the agent.
13+
1014
1. Start with `repo.status`.
1115
2. Use `sdl.context` for task-shaped explain, debug, review, and implement work.
1216
Its request is flat and requires `budget.maxTokens`; never send `options`,
@@ -18,7 +22,9 @@ Use SDL-MCP as the repository boundary.
1822
runtimeExecute executes repository tooling. Permitted uses include build,
1923
test, lint, compiler, named scripts, and targeted edit scripts. Do not use it
2024
to inspect, search, or print repository files. Use sdl.context or sdl.retrieve
21-
for indexed source and sdl.file with op="read" for other files.
25+
for indexed source and sdl.file with op="read" for other files. Do not guess
26+
`runtimeQueryOutput` arguments; replay a returned action unchanged or call
27+
focused `sdl.manual` for `runtime.queryOutput` first.
2228
5. Read non-indexed files through `sdl.file`. Its targeted write operation can
2329
update one indexed file with live reconciliation; prefer symbol or
2430
search-edit preview/apply operations when they can anchor the change.

tests/stress/fixtures/SDL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Use this file as the repository fallback for the `sdl-mcp-agent-workflow` skill.
44

55
SDL-MCP is the normal repository interface. Native filesystem and shell tools are fallback-only when SDL-MCP is unavailable, or when accessing agent memory and other internal client data outside the indexed repository.
66

7+
When a client bridge returns the raw MCP result, prefer `structuredContent`; use text `content` only as a fallback for older servers. Do not emit both or return the whole MCP response envelope to the agent.
8+
79
---
810

911
## 1. Start Every Task
@@ -175,7 +177,7 @@ Use this when you need likely files and symbols before choosing `symbol.edit` or
175177

176178
Use the cheapest rung that answers the task. Static price tags in `sdl.manual` and `sdl.action.search` are release-time estimates, not live telemetry; use them to choose the first probe, then use `usage.stats` and `signalDensity` only when you need a savings or waste report.
177179

178-
- Runtime: prefer `outputMode: "digest"` for build/test/lint and other noisy commands. The digest keeps a compact parsed status and persists full output for `runtimeQueryOutput`.
180+
- Runtime: prefer `outputMode: "digest"` for build/test/lint and other noisy commands. The digest keeps a compact parsed status and persists full output for `runtimeQueryOutput`. Do not guess `runtimeQueryOutput` arguments; replay a returned action unchanged or call focused `sdl.manual` for `runtime.queryOutput` first.
179181
```json
180182
{
181183
"fn": "runtimeExecute",

tests/unit/agent-workflow-sync.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,35 @@ ${result.stderr}`);
9999
}
100100
});
101101

102+
it("keeps raw MCP result handling compact across agent workflow surfaces", () => {
103+
const workflowSurfaces = [
104+
"templates/SDL.md",
105+
"SDL.md",
106+
"tests/stress/fixtures/SDL.md",
107+
"templates/sdl-mcp-agent-workflow/SKILL.md",
108+
"src/mcp/server-instructions.ts",
109+
];
110+
111+
for (const relativePath of workflowSurfaces) {
112+
const content = readFileSync(resolve(repoRoot, relativePath), "utf8");
113+
assert.match(
114+
content,
115+
/prefer `structuredContent`[\s\S]{0,220}fallback[\s\S]{0,120}older servers/i,
116+
relativePath,
117+
);
118+
assert.match(
119+
content,
120+
/do not emit both[\s\S]{0,120}whole MCP response envelope/i,
121+
relativePath,
122+
);
123+
assert.match(
124+
content,
125+
/Do not guess\s+`runtimeQueryOutput` arguments[\s\S]{0,180}sdl\.manual/i,
126+
relativePath,
127+
);
128+
}
129+
});
130+
102131
it("keeps fallback workflow readiness and provenance guidance current", () => {
103132
const workflowSurfaces = [
104133
"templates/SDL.md",

tests/unit/response-projection-inventory.test.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
zodToSchemaSummary,
2121
} from "../../dist/code-mode/action-catalog.js";
2222
import { getActiveFnNameMap } from "../../dist/code-mode/manual-generator.js";
23-
import { projectToolResultForModelContent } from "../../dist/mcp/context-response-projection.js";
23+
import {
24+
projectToolResultForModelContent,
25+
projectWorkflowChildResultForModel,
26+
} from "../../dist/mcp/context-response-projection.js";
2427
import { OUTPUT_BUDGET_TOKEN_LIMITS } from "../../dist/mcp/response-projection/budgets.js";
2528
import { buildValidatedRecoveryAction } from "../../dist/mcp/response-projection/recovery.js";
2629
import {
@@ -799,6 +802,74 @@ describe("response projection inventory", () => {
799802
assert.deepEqual(workflowOutputSchema.parse(projected), projected);
800803
});
801804

805+
it("accepts every projected workflow child fixture", () => {
806+
const workflowRegistration = capturePublicToolRegistrations({
807+
enabled: true,
808+
exclusive: true,
809+
}).find(({ name }) => name === "sdl.workflow");
810+
assert.ok(workflowRegistration);
811+
const workflowOutputSchema = exhaustiveOutputSchema(workflowRegistration);
812+
assert.ok(workflowOutputSchema);
813+
814+
const fnByAction = new Map<string, string>([
815+
...Object.entries(getActiveFnNameMap(true)).map(
816+
([fn, action]) => [action, fn] as const,
817+
),
818+
...Object.keys(INTERNAL_TRANSFORM_OUTPUT_SCHEMA_BY_ACTION).map(
819+
(action) => [action, action] as const,
820+
),
821+
]);
822+
const coveredActions = new Set<string>();
823+
const failures: string[] = [];
824+
825+
for (const fixture of AGENT_OUTPUT_CASES) {
826+
const fn = fnByAction.get(fixture.action);
827+
if (fn === undefined) continue;
828+
coveredActions.add(fixture.action);
829+
for (const detail of ["compact", "full"] as const) {
830+
const workflowArgs = {
831+
repoId: "projection-fixture",
832+
detail,
833+
includeDiagnostics: false,
834+
steps: [{ fn, args: fixture.publicRequest, detail }],
835+
};
836+
const childResult = projectWorkflowChildResultForModel(
837+
fn,
838+
fixture.canonicalResultFactory(),
839+
workflowArgs,
840+
{ ...fixture.publicRequest, detail },
841+
);
842+
const projected = projectToolResultForModelContent(
843+
"sdl.workflow",
844+
{
845+
results: [{
846+
stepIndex: 0,
847+
fn,
848+
status: "ok",
849+
result: childResult,
850+
_resolvedArgs: fixture.publicRequest,
851+
}],
852+
},
853+
workflowArgs,
854+
);
855+
const parsed = workflowOutputSchema.safeParse(projected);
856+
if (!parsed.success) {
857+
failures.push(
858+
`${fixture.action}/${detail}: projected=${JSON.stringify(projected)} issues=${JSON.stringify(parsed.error.issues)}`,
859+
);
860+
} else {
861+
assert.deepEqual(parsed.data, projected, `${fixture.action}/${detail}`);
862+
}
863+
}
864+
}
865+
866+
assert.deepEqual(
867+
[...coveredActions].sort(),
868+
[...new Set(fnByAction.keys())].sort(),
869+
);
870+
assert.deepEqual(failures, []);
871+
});
872+
802873
it("rejects arbitrary response content schemas and incoherent continuations", () => {
803874
const registrations = capturePublicToolRegistrations();
804875
const responseRegistration = registrations.find(

tests/unit/server-unit.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,23 @@ describe("MCPServer", () => {
285285
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /sdl-mcp-agent-workflow/);
286286
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /repo\.status/);
287287
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /sdl\.workflow[^\n]*repoStatus/);
288+
assert.match(
289+
SDL_MCP_SERVER_INSTRUCTIONS,
290+
/prefer `structuredContent`[^\n]*fallback[^\n]*older servers/i,
291+
);
292+
assert.match(
293+
SDL_MCP_SERVER_INSTRUCTIONS,
294+
/do not emit both[^\n]*whole MCP response envelope/i,
295+
);
288296
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /sdl\.action\.search/);
289297
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /sdl\.context/);
290298
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /usageStats/);
291299
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /outputMode: \"digest\"/);
292300
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /outputMode: \"minimal\"/);
301+
assert.match(
302+
SDL_MCP_SERVER_INSTRUCTIONS,
303+
/Do not guess `runtimeQueryOutput` arguments[^\n]*sdl\.manual/i,
304+
);
293305
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /refsMode: \"off\"/);
294306
assert.match(SDL_MCP_SERVER_INSTRUCTIONS, /short ids/);
295307
assert.match(

0 commit comments

Comments
 (0)