From 6c3d81b03902fd3a4800d56f073ecfea1778d177 Mon Sep 17 00:00:00 2001 From: Nuno Aguiar Date: Sat, 28 Mar 2026 02:07:52 +0000 Subject: [PATCH 1/2] Add MCP client protocol data to getClientInfo --- js/openaf.js | 29 +++++++++++++++++++++++++++++ tests/autoTestAll.MCP.js | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/js/openaf.js b/js/openaf.js index 0318689c..76b3626b 100644 --- a/js/openaf.js +++ b/js/openaf.js @@ -8503,6 +8503,13 @@ const $jsonrpc = function (aOptions) { mcpSessionId: __ } + const _mcpInfo = { + session: _session, + lastRequest: __, + lastResponse: __, + lastResponseHeaders: __ + } + const _captureSessionFromHeaders = headers => { var _sid = _pickHeaderCaseInsensitive(headers, "mcp-session-id") if (isDef(_sid) && String(_sid).length > 0) { @@ -8680,6 +8687,11 @@ const $jsonrpc = function (aOptions) { }, exec: (aMethod, aParams, aNotification, aExecOptions) => { aExecOptions = _$(aExecOptions, "aExecOptions").isMap().default({}) + _mcpInfo.lastRequest = { + method: _$(aMethod, "aMethod").isString().default(__), + params: _$(aParams, "aParams").isMap().default({}), + notification: !!aNotification + } switch (aOptions.type) { case "dummy": aOptions.options = _$(aOptions.options, "aOptions.options").isMap().default({}) @@ -8690,6 +8702,8 @@ const $jsonrpc = function (aOptions) { if (isMap(aOptions.options.fns)) { if (isFunction(aOptions.options.fns[aMethod])) { var _res = aOptions.options.fns[aMethod](aParams) + _mcpInfo.lastResponse = { result: _res } + _mcpInfo.lastResponseHeaders = __ _debug("jsonrpc dummy <- " + stringify({ result: _res }, __, "")) if (aMethod == "initialize" && !aNotification) _r._info = _res return _res @@ -8730,6 +8744,8 @@ const $jsonrpc = function (aOptions) { _res = _r._r[_id] delete _r._r[_id] } + _mcpInfo.lastResponse = _res + _mcpInfo.lastResponseHeaders = __ if (aMethod == "initialize" && !aNotification) _r._info = isDef(_res) && isDef(_res.result) ? _res.result : _res return isDef(_res) && isDef(_res.result) ? _res.result : _res case "sse": @@ -8774,6 +8790,8 @@ const $jsonrpc = function (aOptions) { if (!!aNotification) { var _notificationRes = $rest(_restOptions).post2Stream(aOptions.url, _req) _captureSessionFromHeaders(_http.responseHeaders()) + _mcpInfo.lastResponseHeaders = clone(_http.responseHeaders()) + _mcpInfo.lastResponse = __ if (isDef(_notificationRes) && "function" === typeof _notificationRes.close) { try { _notificationRes.close() } catch(e) {} } @@ -8781,6 +8799,7 @@ const $jsonrpc = function (aOptions) { } var _streamRes = $rest(_restOptions).post2Stream(aOptions.url, _req) _captureSessionFromHeaders(_http.responseHeaders()) + _mcpInfo.lastResponseHeaders = clone(_http.responseHeaders()) var _events = _r._readSSE(_streamRes) res = _events.filter(r => isMap(r)).filter(r => r.id == _req.id || isUnDef(r.id)).shift() if (isUnDef(res) && _events.length > 0) res = _events[0] @@ -8789,7 +8808,9 @@ const $jsonrpc = function (aOptions) { _restOptions.httpClient = _http res = $rest(_restOptions).post(aOptions.url, _req) _captureSessionFromHeaders(_http.responseHeaders()) + _mcpInfo.lastResponseHeaders = clone(_http.responseHeaders()) } + _mcpInfo.lastResponse = res // Notifications do not expect a reply if (!!aNotification) return _debug("jsonrpc <- " + stringify(res, __, "")) @@ -8802,6 +8823,7 @@ const $jsonrpc = function (aOptions) { } }, getInfo: () => _r._info, + getClientInfo: () => merge({}, _mcpInfo), destroy: () => { if (_r._copies.get() > 0) { _r._copies.dec() @@ -9448,6 +9470,13 @@ const $mcp = function(aOptions) { } }, getInfo: () => _r._initResult, + getClientInfo: () => { + var _clientInfo = _jsonrpc.getClientInfo() + if (isMap(_r._initResult)) { + _clientInfo.initialize = clone(_r._initResult) + } + return _clientInfo + }, listTools: () => { if (!_r._initialized) { throw new Error("MCP client not initialized. Call initialize() first.") diff --git a/tests/autoTestAll.MCP.js b/tests/autoTestAll.MCP.js index 574b035a..3d30b064 100644 --- a/tests/autoTestAll.MCP.js +++ b/tests/autoTestAll.MCP.js @@ -84,11 +84,14 @@ if (isNotification) return ow.server.httpd.reply("", 204, "text/plain", {}); + var responseHeaders = {}; + if (rpc.method == "initialize") responseHeaders["mcp-session-id"] = "test-session-1"; + return ow.server.httpd.reply({ jsonrpc: "2.0", result: result, id: rpc.id - }, 200, "application/json", {}); + }, 200, "application/json", responseHeaders); } }); @@ -199,4 +202,35 @@ } }); }; + + exports.testGetClientInfoIncludesJSONRPCMCPData = function() { + withOAuthMCPServer(function(ctx) { + var client = $mcp({ + type: "remote", + strict: false, + url: ctx.resource, + auth: { + type: "oauth2", + grantType: "client_credentials", + clientId: "client-a", + clientSecret: "secret-a" + } + }); + + try { + client.initialize({ name: "TestClient", version: "9.9.9" }); + var info = client.getClientInfo(); + + ow.test.assert(info.lastRequest.method, "initialize", "getClientInfo should include the last JSON-RPC method."); + ow.test.assert(info.lastRequest.params.protocolVersion, "2024-11-05", "getClientInfo should include initialize protocolVersion."); + ow.test.assert(info.lastRequest.params.clientInfo.name, "TestClient", "getClientInfo should include sent clientInfo."); + ow.test.assert(info.lastResponse.jsonrpc, "2.0", "getClientInfo should include raw JSON-RPC response envelope."); + ow.test.assert(info.lastResponse.result.serverInfo.name, "OAuth MCP", "getClientInfo should include JSON-RPC result data."); + ow.test.assert(info.session.mcpSessionId, "test-session-1", "getClientInfo should include captured MCP session id."); + ow.test.assert(info.initialize.protocolVersion, "2024-11-05", "getClientInfo should include initialize result data."); + } finally { + client.destroy(); + } + }); + }; })(); From 989266576cb0a9c0ed64df6bf3e78f9a902b012e Mon Sep 17 00:00:00 2001 From: nmaguiar Date: Sat, 28 Mar 2026 03:15:36 +0000 Subject: [PATCH 2/2] Enhance MCP functionality by adding synthetic capabilities and server info to getClientInfo --- js/openaf.js | 69 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/js/openaf.js b/js/openaf.js index 76b3626b..15193f80 100644 --- a/js/openaf.js +++ b/js/openaf.js @@ -8702,13 +8702,15 @@ const $jsonrpc = function (aOptions) { if (isMap(aOptions.options.fns)) { if (isFunction(aOptions.options.fns[aMethod])) { var _res = aOptions.options.fns[aMethod](aParams) - _mcpInfo.lastResponse = { result: _res } + _mcpInfo.lastResponse = { jsonrpc: "2.0", result: _res } _mcpInfo.lastResponseHeaders = __ - _debug("jsonrpc dummy <- " + stringify({ result: _res }, __, "")) + _debug("jsonrpc dummy <- " + stringify(_mcpInfo.lastResponse, __, "")) if (aMethod == "initialize" && !aNotification) _r._info = _res return _res } else { - _debug("jsonrpc dummy <- " + stringify({ error: "Method not found" }, __, "")) + _mcpInfo.lastResponse = { jsonrpc: "2.0", error: { code: -32601, message: "Method not found" } } + _mcpInfo.lastResponseHeaders = __ + _debug("jsonrpc dummy <- " + stringify(_mcpInfo.lastResponse, __, "")) throw new Error("Method not found") } } @@ -9027,6 +9029,31 @@ const $mcp = function(aOptions) { _toolBlacklist[toolName] = true }) + const _buildSyntheticCapabilities = fns => { + var _caps = {} + fns = _$(fns, "fns").isMap().default({}) + var _fnNames = Object.keys(fns) + var _hasCustomTools = _fnNames.some(name => ["tools/list", "tools/call", "initialize", "notifications/initialized"].indexOf(name) < 0) + if (_hasCustomTools || isDef(fns["tools/list"]) || isDef(fns["tools/call"])) _caps.tools = { listChanged: false } + if (isDef(fns["prompts/list"]) || isDef(fns["prompts/get"])) _caps.prompts = { listChanged: false } + if (isDef(fns["resources/list"]) || isDef(fns["resources/read"]) || isDef(fns["resources/templates/list"])) _caps.resources = { listChanged: false } + if (isDef(fns["logging/setLevel"])) _caps.logging = {} + if (isDef(fns["agents/list"]) || isDef(fns["agents/get"]) || isDef(fns["agents/send"])) _caps.agents = {} + return _caps + } + + const _buildSyntheticInitializeResult = opts => { + opts = _$(opts, "opts").isMap().default({}) + var _result = { + protocolVersion: _$(opts.protocolVersion, "opts.protocolVersion").isString().default(aOptions.protocolVersion) + } + var _serverInfo = _$(opts.serverInfo, "opts.serverInfo").isMap().default(__) + var _capabilities = _$(opts.capabilities, "opts.capabilities").isMap().default(__) + if (isMap(_serverInfo)) _result.serverInfo = clone(_serverInfo) + if (isMap(_capabilities)) _result.capabilities = clone(_capabilities) + return _result + } + const _defaultCmdDir = (isDef(__flags) && isDef(__flags.JSONRPC) && isDef(__flags.JSONRPC.cmd) && isDef(__flags.JSONRPC.cmd.defaultDir)) ? __flags.JSONRPC.cmd.defaultDir : __ const _isToolBlacklisted = toolName => _toolBlacklist[toolName] === true const _filterToolsList = toolsRes => { @@ -9283,7 +9310,7 @@ const $mcp = function(aOptions) { aOptions.options.init = _$(aOptions.options.init, "aOptions.options.init").default(__) // Load jobs from the oJob - var fnsMeta = {}, fns = {} + var fnsMeta = {}, fns = {}, _initMeta = {} var jobsTemp = io.createTempFile("ojob_mcp_", ".yaml") var jobsPreD = aOptions.options.job.endsWith(".json") ? io.readFileJSON(_defaultCmdDir + String(java.io.File.separator) + aOptions.options.job) : io.readFileYAML(_defaultCmdDir + String(java.io.File.separator) + aOptions.options.job) if (isDef(jobsPreD.ojob) && isDef(jobsPreD.ojob.daemon)) delete jobsPreD.ojob.daemon @@ -9340,8 +9367,30 @@ const $mcp = function(aOptions) { throw new Error("Invalid oJob data loaded from " + aOptions.options.job) } + var _serverInfoEntries = searchKeys(jobsPreD, "serverInfo") + if (isMap(_serverInfoEntries)) { + var _serverInfoKey = Object.keys(_serverInfoEntries).find(k => isMap(_serverInfoEntries[k])) + if (isDef(_serverInfoKey)) _initMeta.serverInfo = clone(_serverInfoEntries[_serverInfoKey]) + } + var _capabilitiesEntries = searchKeys(jobsPreD, "capabilities") + if (isMap(_capabilitiesEntries)) { + var _capabilitiesKey = Object.keys(_capabilitiesEntries).find(k => isMap(_capabilitiesEntries[k])) + if (isDef(_capabilitiesKey)) _initMeta.capabilities = clone(_capabilitiesEntries[_capabilitiesKey]) + } + var _protocolVersionEntries = searchKeys(jobsPreD, "protocolVersion") + if (isMap(_protocolVersionEntries)) { + var _protocolVersionKey = Object.keys(_protocolVersionEntries).find(k => isString(_protocolVersionEntries[k])) + if (isDef(_protocolVersionKey)) _initMeta.protocolVersion = String(_protocolVersionEntries[_protocolVersionKey]) + } + aOptions.type = "dummy" aOptions.options.fnsMeta = fnsMeta + aOptions.options.serverInfo = _$(aOptions.options.serverInfo, "aOptions.options.serverInfo").isMap().default(_$( _initMeta.serverInfo, "_initMeta.serverInfo").isMap().default({ + name: "OpenAF oJob MCP", + version: "1.0.0" + })) + aOptions.options.capabilities = _$(aOptions.options.capabilities, "aOptions.options.capabilities").isMap().default(_$( _initMeta.capabilities, "_initMeta.capabilities").isMap().default(_buildSyntheticCapabilities(fns))) + aOptions.options.protocolVersion = _$(aOptions.options.protocolVersion, "aOptions.options.protocolVersion").isString().default(_$( _initMeta.protocolVersion, "_initMeta.protocolVersion").isString().default(aOptions.protocolVersion)) aOptions.options.fns["tools/list"] = params => { return { tools: Object.keys(fns) @@ -9355,7 +9404,7 @@ const $mcp = function(aOptions) { return $job(fns[params.name], params.arguments) } aOptions.options.fns["initialize"] = params => { - return { protocolVersion: "2024-11-05" } + return _buildSyntheticInitializeResult(aOptions.options) } aOptions.options.fns["notifications/initialized"] = params => { return {} @@ -9364,6 +9413,12 @@ const $mcp = function(aOptions) { aOptions.options = _$(aOptions.options, "aOptions.options").isMap().default({}) aOptions.options.fns = _$(aOptions.options.fns, "aOptions.options.fns").isMap().default({}) aOptions.options.fnsMeta = _$(aOptions.options.fnsMeta, "aOptions.options.fnsMeta").isMap().default({}) + aOptions.options.serverInfo = _$(aOptions.options.serverInfo, "aOptions.options.serverInfo").isMap().default({ + name: "OpenAF Dummy MCP", + version: "1.0.0" + }) + aOptions.options.capabilities = _$(aOptions.options.capabilities, "aOptions.options.capabilities").isMap().default(_buildSyntheticCapabilities(aOptions.options.fns)) + aOptions.options.protocolVersion = _$(aOptions.options.protocolVersion, "aOptions.options.protocolVersion").isString().default(aOptions.protocolVersion) aOptions.options.fns["tools/list"] = params => { return { @@ -9378,7 +9433,7 @@ const $mcp = function(aOptions) { return aOptions.options.fns[params.name](params.arguments) } aOptions.options.fns["initialize"] = params => { - return { protocolVersion: "2024-11-05" } + return _buildSyntheticInitializeResult(aOptions.options) } aOptions.options.fns["notifications/initialized"] = params => { return {} @@ -9532,7 +9587,7 @@ const $mcp = function(aOptions) { * $mcp.listAgents() : Map * Lists all available A2A agents from the MCP server.\ * Returns a map with an 'agents' array containing agent metadata (id, name, title, version, tags, capabilities).\ - * Must call initialize() first.\ + return _buildSyntheticInitializeResult(aOptions.options) * \ * Example:\ * \