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
96 changes: 90 additions & 6 deletions js/openaf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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({})
Expand All @@ -8690,11 +8702,15 @@ const $jsonrpc = function (aOptions) {
if (isMap(aOptions.options.fns)) {
if (isFunction(aOptions.options.fns[aMethod])) {
var _res = aOptions.options.fns[aMethod](aParams)
_debug("jsonrpc dummy <- " + stringify({ result: _res }, __, ""))
_mcpInfo.lastResponse = { jsonrpc: "2.0", result: _res }
_mcpInfo.lastResponseHeaders = __
_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")
}
}
Expand Down Expand Up @@ -8730,6 +8746,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":
Expand Down Expand Up @@ -8774,13 +8792,16 @@ 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) {}
}
return
}
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]
Expand All @@ -8789,7 +8810,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, __, ""))
Expand All @@ -8802,6 +8825,7 @@ const $jsonrpc = function (aOptions) {
}
},
getInfo: () => _r._info,
getClientInfo: () => merge({}, _mcpInfo),
destroy: () => {
if (_r._copies.get() > 0) {
_r._copies.dec()
Expand Down Expand Up @@ -9005,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 => {
Expand Down Expand Up @@ -9261,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
Expand Down Expand Up @@ -9318,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)
Expand All @@ -9333,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 {}
Expand All @@ -9342,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 {
Expand All @@ -9356,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 {}
Expand Down Expand Up @@ -9448,6 +9525,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.")
Expand Down Expand Up @@ -9503,7 +9587,7 @@ const $mcp = function(aOptions) {
* <key>$mcp.listAgents() : Map</key>
* 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:\
* \
Expand Down
36 changes: 35 additions & 1 deletion tests/autoTestAll.MCP.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});

Expand Down Expand Up @@ -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();
}
});
};
})();
Loading