Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
119 changes: 118 additions & 1 deletion actions/setup/js/mcp_cli_bridge.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,118 @@ function showToolHelp(serverName, toolName, tools) {
// Response formatting
// ---------------------------------------------------------------------------

/**
* Extract JSON-RPC messages from a response body that may be:
* - A JSON object
* - A JSON string
* - Server-Sent Events (SSE) payload containing multiple `data:` lines
*
* @param {unknown} responseBody
* @returns {unknown[]}
*/
function extractJSONRPCMessages(responseBody) {
if (responseBody == null) {
return [];
}

if (Array.isArray(responseBody)) {
return responseBody;
}

if (typeof responseBody === "object") {
return [responseBody];
}

if (typeof responseBody !== "string") {
return [];
}

const trimmed = responseBody.trim();
if (!trimmed) {
return [];
}

try {
return [JSON.parse(trimmed)];
} catch {
// Fall through to SSE parsing.
}

/** @type {unknown[]} */
const messages = [];
for (const line of trimmed.split(/\r?\n/)) {
if (!line.startsWith("data:")) {
continue;
}
const payload = line.slice(5).trim();
if (!payload || payload === "[DONE]") {
continue;
}
try {
messages.push(JSON.parse(payload));
} catch {
// Ignore non-JSON SSE data lines.
}
}

return messages;
}

/**
* Render MCP progress notifications to stderr.
*
* @param {unknown[]} messages - Parsed JSON-RPC message stream
*/
function renderProgressMessages(messages) {
for (const message of messages) {
if (!message || typeof message !== "object" || !("method" in message) || message.method !== "notifications/progress") {
continue;
}

const params = "params" in message && message.params && typeof message.params === "object" ? message.params : null;
if (!params) {
continue;
}

const progressText = "message" in params && params.message ? String(params.message) : "";
const progress = "progress" in params && typeof params.progress === "number" ? params.progress : null;
const total = "total" in params && typeof params.total === "number" ? params.total : null;

if (progressText) {
process.stderr.write(progressText + "\n");
continue;
}

if (progress != null && total != null) {
process.stderr.write(`Progress: ${progress}/${total}\n`);
continue;
}

if (progress != null) {
process.stderr.write(`Progress: ${progress}\n`);
continue;
}

process.stderr.write(`Progress: ${JSON.stringify(params)}\n`);
}
}

/**
* @param {unknown} message
* @returns {boolean}
*/
function isErrorMessage(message) {
return !!(message && typeof message === "object" && "error" in message);
}

/**
* @param {unknown} message
* @returns {boolean}
*/
function isResultMessage(message) {
return !!(message && typeof message === "object" && "result" in message);
}

/**
* Format and display the MCP tool call response.
*
Expand All @@ -740,7 +852,10 @@ function showToolHelp(serverName, toolName, tools) {
*/
function formatResponse(responseBody, serverName) {
const core = global.core;
const resp = responseBody;
const messages = extractJSONRPCMessages(responseBody);
renderProgressMessages(messages);

const resp = messages.find(isErrorMessage) || messages.find(isResultMessage) || responseBody;

// Check for JSON-RPC error
if (resp && typeof resp === "object" && "error" in resp && resp.error && typeof resp.error === "object") {
Expand Down Expand Up @@ -905,6 +1020,8 @@ if (require.main === module) {
module.exports = {
parseToolArgs,
coerceToolArgValue,
extractJSONRPCMessages,
renderProgressMessages,
formatResponse,
main,
};
24 changes: 24 additions & 0 deletions actions/setup/js/mcp_cli_bridge.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,28 @@ describe("mcp_cli_bridge.cjs", () => {
expect(stderrChunks.join("")).toContain("failed to audit workflow run");
expect(process.exitCode).toBe(1);
});

it("prints progress notifications to stderr and final text result to stdout for SSE responses", () => {
const sseBody = [
'data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"abc","progress":1,"total":3,"message":"Step 1/3"}}',
'data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"done"}]}}',
"",
].join("\n");

formatResponse(sseBody, "agenticworkflows");

expect(stderrChunks.join("")).toContain("Step 1/3");
expect(stdoutChunks.join("")).toBe("done\n");
expect(process.exitCode).toBe(0);
});

it("prints numeric progress to stderr when progress notification has no message", () => {
const sseBody = ['data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"abc","progress":2,"total":5}}', 'data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"ok"}]}}', ""].join("\n");

formatResponse(sseBody, "agenticworkflows");

expect(stderrChunks.join("")).toContain("Progress: 2/5");
expect(stdoutChunks.join("")).toBe("ok\n");
expect(process.exitCode).toBe(0);
});
});
10 changes: 5 additions & 5 deletions pkg/cli/spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1117,11 +1117,11 @@ func TestSpec_PublicAPI_ValidateWorkflowIntent(t *testing.T) {
// Spec: "Sets a field in frontmatter YAML"
func TestSpec_PublicAPI_UpdateFieldInFrontmatter(t *testing.T) {
tests := []struct {
name string
content string
fieldName string
fieldValue string
wantErr bool
name string
content string
fieldName string
fieldValue string
wantErr bool
checkContains string
}{
{
Expand Down
Loading