Before submitting
Area
apps/server
Summary
T3 returns declared MCP tool failures as successful CallToolResult envelopes. The body contains OrchestratorMcpFailure, but isError is false. A failed read-only lookup reproduces this without changing thread state.
This came up when an agent tried to settle its active thread. The lifecycle refusal is correct. This report concerns how the refusal is represented to the MCP client, not permitting active-thread settlement.
Steps to reproduce
-
Use a running T3 thread with the built-in t3-code MCP tools.
-
Call t3_queue_list with a syntactically valid thread ID that does not exist in the calling project:
{"threadId":"00000000-0000-4000-8000-000000000000","limit":1}
-
Inspect the complete MCP result, including isError. It returns the failure below with isError: false.
-
As a success control, call t3_queue_list with {"limit":1} in the current thread. It returns the queue and isError: false.
-
The same failure flag reproduces with t3_thread_read for that nonexistent ID. No pin, archive, settle, provider restart, or database mutation is needed.
Expected behavior
Declared tool execution failures have isError: true. Their representation should also be compatible with the advertised output schema so an MCP client can receive the domain refusal rather than a schema-validation error. Successful calls continue to have isError: false.
Keep lifecycle and authorization guards unchanged. This does not request exposing arbitrary exception text or private storage paths.
Actual behavior
The failed t3_queue_list call returns:
{
"content": [{
"type": "text",
"text": "{\"_tag\":\"OrchestratorMcpFailure\",\"code\":\"orchestration_error\",\"message\":\"The operation could not be completed.\"}"
}],
"structuredContent": {
"_tag": "OrchestratorMcpFailure",
"code": "orchestration_error",
"message": "The operation could not be completed."
},
"isError": false
}
The successful current-thread control returns structuredContent: {"items":[],"nextCursor":null}, also with isError: false.
Impact
Minor bug or occasional failure. Clients relying on the standard error flag cannot distinguish these failures from success. A schema-validating client can replace the useful refusal with a result-schema error.
T3's grouped tool-summary code recognizes _tag: "OrchestratorMcpFailure" independently in packages/shared/src/toolOutput.ts:77-86. Therefore this is not a claim that every T3 UI renders the call as successful. The Pi MCP bridge uses only the outer isError flag at piT3McpExtensionSource.ts:118-124,293; that consequence is source analysis, not a tested Pi session.
Version or commit
Live reproduction: 0.0.46-nightly.20261004.2644.
Source investigation: pingdotgg/t3code main at d60a71ef6e371d8b345ecb1f7575117d675187c7. I have not established that the installed nightly was built from this exact SHA. Both exhibit the relevant behavior.
Environment
T3 Code Nightly desktop, macOS 27.2, arm64, Codex provider. Isolated reproduction uses Node v26.10.0, effect@4.0.0-rc.115 as pinned by T3, and @modelcontextprotocol/sdk@1.32.0.
Logs or stack traces
An isolated call through the actual Effect Tool / Toolkit / McpServer.callTool code reproduces the failure flag. Replaying its unchanged tool definitions and results through the official MCP SDK over an in-memory transport gives:
returned_failure isError=false MCP error -32602: Structured content does not match the tool's output schema: data must have required property 'ok'
raised_failure isError=true accepted
success isError=false accepted
The raised_failure control uses the same declared error with failureMode: "error". It demonstrates that default error handling takes a different path. This is a dependency-level minimization, not a full T3 server integration test.
Diagnosis
At the pinned T3 revision:
- Thread tools declare
failure: OrchestratorMcpFailure and failureMode: "return". The organize tool does too at lines 48-50.
- T3 registers the toolkit through
McpServer.toolkit.
- Effect Toolkit preserves the failed handler result as
isFailure: true.
- Effect's MCP adapter ignores that field and creates
CallToolResult with isError: false. It advertises the success schema at lines 1547-1550. I verified the same implementation in the installed effect@4.0.0-rc.115 package used by the minimization. T3's Effect patch does not modify registerToolkit or this result serialization path.
The existing bounded public failure test asserts the failure body but not isError. Retaining its private-cause redaction is important.
Separately, the original active-settle refusal loses its reason through Effect.mapError(unavailable) in thread/handlers.ts:300. Fixing the envelope alone will not improve that message. That is context, not an additional requested fix in this issue.
Screenshots, recordings, or supporting files
The complete dependency-level reproduction is included below. It opens no listener, starts no provider, and reads no T3 state. Run in an empty temporary directory:
npm init -y
npm install --ignore-scripts --no-audit --no-fund effect@4.0.0-rc.115 @modelcontextprotocol/sdk@1.32.0
node repro.mjs
repro.mjs
import assert from 'node:assert/strict';
import { Effect, Layer, Schema } from 'effect';
import { McpSchema, McpServer, Tool, Toolkit } from 'effect/unstable/ai';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
class Denied extends Schema.TaggedError()('Denied', { message: Schema.String }) {}
const modes = { returned_failure: 'return', raised_failure: 'error', success: 'return' };
const toolkit = Toolkit.make(...Object.entries(modes).map(([name, failureMode]) =>
Tool.make(name, {
parameters: Schema.Struct({ action: Schema.String }),
success: Schema.Struct({ ok: Schema.Boolean }), failure: Denied, failureMode,
})));
const handlers = toolkit.toLayer(Object.fromEntries(Object.keys(modes).map(name => [name,
() => name === 'success' ? Effect.succeed({ ok: true }) :
Effect.fail(new Denied({ message: 'Known domain refusal' })),
])));
const clientContext = McpSchema.McpServerClient.of({
clientId: 1, protocolVersion: '2025-06-18', clientCapabilities: {},
clientInfo: { name: 'repro', version: '1' },
initializePayload: { protocolVersion: '2025-06-18', capabilities: {},
clientInfo: { name: 'repro', version: '1' } }, getClient: Effect.die('unused'),
});
const captured = await Effect.runPromise(Effect.gen(function* () {
const server = yield* McpServer.McpServer;
const results = {};
for (const name of Object.keys(modes)) {
results[name] = yield* server.callTool({ name, arguments: { action: 'test' } }).pipe(
Effect.provideService(McpSchema.McpServerClient, clientContext));
}
return { tools: server.tools.map(({ tool }) => tool), results };
}).pipe(Effect.provide(McpServer.toolkit(toolkit).pipe(
Layer.provide(handlers), Layer.provideMerge(McpServer.McpServer.layer)))));
assert.equal(captured.results.returned_failure.isError, false);
assert.equal(captured.results.raised_failure.isError, true);
assert.equal(captured.results.success.isError, false);
// Replay the actual Effect outputs and tool definitions without rewriting either.
const server = new Server({ name: 'replay', version: '1' }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: captured.tools }));
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => captured.results[params.name]);
const client = new Client({ name: 'repro-client', version: '1' });
const [a, b] = InMemoryTransport.createLinkedPair();
try {
await server.connect(b);
await client.connect(a);
await client.listTools();
for (const name of Object.keys(modes)) {
let message;
try {
await client.callTool({ name, arguments: { action: 'test' } });
message = 'accepted';
} catch (error) { message = error.message; }
assert.equal(message === 'accepted', name !== 'returned_failure');
if (name === 'returned_failure') assert.match(message, /Structured content does not match/);
console.log(name, 'isError=' + captured.results[name].isError, message);
}
} finally { await client.close(); await server.close(); }
Workaround
An MCP consumer can inspect the known typed failure body in addition to isError. This only helps when the client exposes that body; it does not resolve output-schema validation. For settlement itself, wait for the run to finish and use the normal app lifecycle control.
Related reports and scope
Searched issues and PRs for isError, failureMode, OrchestratorMcpFailure, the generic error text, MCP failure/success, and output-schema failures. I found no report for this specific path.
Verification limits and authorship
Live read-only reproductions and isolated dependency/client tests were run. No full T3 integration suite, provider UI matrix, or fix was tested. No running installation or database was changed.
Investigation and reporting by GPT-6 Astra in the Codex harness through T3 Code, with independent source reviews from GPT-6.1 Sol, Cursor Auto, and Kimi K3 through their direct provider tools. This was not filed through npx t3 triage.
Before submitting
Area
apps/server
Summary
T3 returns declared MCP tool failures as successful
CallToolResultenvelopes. The body containsOrchestratorMcpFailure, butisErrorisfalse. A failed read-only lookup reproduces this without changing thread state.This came up when an agent tried to settle its active thread. The lifecycle refusal is correct. This report concerns how the refusal is represented to the MCP client, not permitting active-thread settlement.
Steps to reproduce
Use a running T3 thread with the built-in
t3-codeMCP tools.Call
t3_queue_listwith a syntactically valid thread ID that does not exist in the calling project:{"threadId":"00000000-0000-4000-8000-000000000000","limit":1}Inspect the complete MCP result, including
isError. It returns the failure below withisError: false.As a success control, call
t3_queue_listwith{"limit":1}in the current thread. It returns the queue andisError: false.The same failure flag reproduces with
t3_thread_readfor that nonexistent ID. No pin, archive, settle, provider restart, or database mutation is needed.Expected behavior
Declared tool execution failures have
isError: true. Their representation should also be compatible with the advertised output schema so an MCP client can receive the domain refusal rather than a schema-validation error. Successful calls continue to haveisError: false.Keep lifecycle and authorization guards unchanged. This does not request exposing arbitrary exception text or private storage paths.
Actual behavior
The failed
t3_queue_listcall returns:{ "content": [{ "type": "text", "text": "{\"_tag\":\"OrchestratorMcpFailure\",\"code\":\"orchestration_error\",\"message\":\"The operation could not be completed.\"}" }], "structuredContent": { "_tag": "OrchestratorMcpFailure", "code": "orchestration_error", "message": "The operation could not be completed." }, "isError": false }The successful current-thread control returns
structuredContent: {"items":[],"nextCursor":null}, also withisError: false.Impact
Minor bug or occasional failure. Clients relying on the standard error flag cannot distinguish these failures from success. A schema-validating client can replace the useful refusal with a result-schema error.
T3's grouped tool-summary code recognizes
_tag: "OrchestratorMcpFailure"independently inpackages/shared/src/toolOutput.ts:77-86. Therefore this is not a claim that every T3 UI renders the call as successful. The Pi MCP bridge uses only the outerisErrorflag atpiT3McpExtensionSource.ts:118-124,293; that consequence is source analysis, not a tested Pi session.Version or commit
Live reproduction:
0.0.46-nightly.20261004.2644.Source investigation:
pingdotgg/t3codemain atd60a71ef6e371d8b345ecb1f7575117d675187c7. I have not established that the installed nightly was built from this exact SHA. Both exhibit the relevant behavior.Environment
T3 Code Nightly desktop, macOS 27.2, arm64, Codex provider. Isolated reproduction uses Node v26.10.0,
effect@4.0.0-rc.115as pinned by T3, and@modelcontextprotocol/sdk@1.32.0.Logs or stack traces
An isolated call through the actual Effect
Tool/Toolkit/McpServer.callToolcode reproduces the failure flag. Replaying its unchanged tool definitions and results through the official MCP SDK over an in-memory transport gives:The
raised_failurecontrol uses the same declared error withfailureMode: "error". It demonstrates that default error handling takes a different path. This is a dependency-level minimization, not a full T3 server integration test.Diagnosis
At the pinned T3 revision:
failure: OrchestratorMcpFailureandfailureMode: "return". The organize tool does too at lines 48-50.McpServer.toolkit.isFailure: true.CallToolResultwithisError: false. It advertises the success schema at lines 1547-1550. I verified the same implementation in the installedeffect@4.0.0-rc.115package used by the minimization. T3's Effect patch does not modifyregisterToolkitor this result serialization path.The existing bounded public failure test asserts the failure body but not
isError. Retaining its private-cause redaction is important.Separately, the original active-settle refusal loses its reason through
Effect.mapError(unavailable)inthread/handlers.ts:300. Fixing the envelope alone will not improve that message. That is context, not an additional requested fix in this issue.Screenshots, recordings, or supporting files
The complete dependency-level reproduction is included below. It opens no listener, starts no provider, and reads no T3 state. Run in an empty temporary directory:
repro.mjs
Workaround
An MCP consumer can inspect the known typed failure body in addition to
isError. This only helps when the client exposes that body; it does not resolve output-schema validation. For settlement itself, wait for the run to finish and use the normal app lifecycle control.Related reports and scope
structuredContent: null, which violates the MCP spec and makes every client report a failure #5583 concernedstructuredContent: nullfrom void preview tools. This report concerns declared failure values being emitted under a success envelope.Searched issues and PRs for
isError,failureMode,OrchestratorMcpFailure, the generic error text, MCP failure/success, and output-schema failures. I found no report for this specific path.Verification limits and authorship
Live read-only reproductions and isolated dependency/client tests were run. No full T3 integration suite, provider UI matrix, or fix was tested. No running installation or database was changed.
Investigation and reporting by GPT-6 Astra in the Codex harness through T3 Code, with independent source reviews from GPT-6.1 Sol, Cursor Auto, and Kimi K3 through their direct provider tools. This was not filed through
npx t3 triage.