Skip to content

Commit 9438d3d

Browse files
authored
Merge pull request #1779 from OpenAF/codex/check-mcp-client-for-sse-bugs
Propagate MCP session id for remote/SSE JSON-RPC calls
2 parents b8300b3 + 50747f3 commit 9438d3d

3 files changed

Lines changed: 117 additions & 2 deletions

File tree

js/openaf.js

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8486,6 +8486,27 @@ const $jsonrpc = function (aOptions) {
84868486
if (aOptions.debug) printErr(ansiColor("yellow,BOLD", "DEBUG: ") + ansiColor("yellow", m))
84878487
}
84888488

8489+
const _pickHeaderCaseInsensitive = (headers, keyName) => {
8490+
if (!isMap(headers)) return __
8491+
var _target = String(keyName).toLowerCase()
8492+
var _foundKey = Object.keys(headers).find(k => String(k).toLowerCase() == _target)
8493+
if (isUnDef(_foundKey)) return __
8494+
var _v = headers[_foundKey]
8495+
if (Array.isArray(_v)) return _v.length > 0 ? _v[0] : __
8496+
return _v
8497+
}
8498+
8499+
const _session = {
8500+
mcpSessionId: __
8501+
}
8502+
8503+
const _captureSessionFromHeaders = headers => {
8504+
var _sid = _pickHeaderCaseInsensitive(headers, "mcp-session-id")
8505+
if (isDef(_sid) && String(_sid).length > 0) {
8506+
_session.mcpSessionId = String(_sid)
8507+
}
8508+
}
8509+
84898510
const _defaultCmdDir = (isDef(__flags) && isDef(__flags.JSONRPC) && isDef(__flags.JSONRPC.cmd) && isDef(__flags.JSONRPC.cmd.defaultDir)) ? __flags.JSONRPC.cmd.defaultDir : __
84908511

84918512
const _r = {
@@ -8718,6 +8739,13 @@ const $jsonrpc = function (aOptions) {
87188739
aParams = _$(aParams, "aParams").isMap().default({})
87198740
var _restOptions = clone(aOptions.options)
87208741
if (isMap(aExecOptions.restOptions)) _restOptions = merge(_restOptions, aExecOptions.restOptions)
8742+
_restOptions.requestHeaders = _$(
8743+
_restOptions.requestHeaders,
8744+
"requestHeaders"
8745+
).isMap().default({})
8746+
if (isDef(_session.mcpSessionId) && isUnDef(_pickHeaderCaseInsensitive(_restOptions.requestHeaders, "mcp-session-id"))) {
8747+
_restOptions.requestHeaders["mcp-session-id"] = _session.mcpSessionId
8748+
}
87218749

87228750
var _req = {
87238751
jsonrpc: "2.0",
@@ -8734,23 +8762,30 @@ const $jsonrpc = function (aOptions) {
87348762
var _useSSE = (aOptions.type == "sse" || aOptions.sse)
87358763
var res
87368764
if (_useSSE) {
8765+
var _http = ow.loadObj().rest.connectionFactory()
8766+
_restOptions.httpClient = _http
87378767
_restOptions.requestHeaders = merge(
87388768
{ Accept: "application/json, text/event-stream" },
87398769
_$(_restOptions.requestHeaders, "requestHeaders").isMap().default({})
87408770
)
87418771
if (!!aNotification) {
87428772
var _notificationRes = $rest(_restOptions).post2Stream(aOptions.url, _req)
8773+
_captureSessionFromHeaders(_http.responseHeaders())
87438774
if (isDef(_notificationRes) && "function" === typeof _notificationRes.close) {
87448775
try { _notificationRes.close() } catch(e) {}
87458776
}
87468777
return
87478778
}
87488779
var _streamRes = $rest(_restOptions).post2Stream(aOptions.url, _req)
8780+
_captureSessionFromHeaders(_http.responseHeaders())
87498781
var _events = _r._readSSE(_streamRes)
87508782
res = _events.filter(r => isMap(r)).filter(r => r.id == _req.id || isUnDef(r.id)).shift()
87518783
if (isUnDef(res) && _events.length > 0) res = _events[0]
87528784
} else {
8785+
var _http = ow.loadObj().rest.connectionFactory()
8786+
_restOptions.httpClient = _http
87538787
res = $rest(_restOptions).post(aOptions.url, _req)
8788+
_captureSessionFromHeaders(_http.responseHeaders())
87548789
}
87558790
// Notifications do not expect a reply
87568791
if (!!aNotification) return
@@ -8810,6 +8845,7 @@ const $jsonrpc = function (aOptions) {
88108845
* - sse (boolean): When true, remote/http MCP requests expect Server-Sent Events responses carrying JSON-RPC payloads\
88118846
* - strict (boolean): Enable strict MCP protocol compliance (default: true)\
88128847
* - clientInfo (map): Client information sent during initialization (default: {name: "OpenAF MCP Client", version: "1.0.0"})\
8848+
* - blacklist (array): Optional array of MCP tool names to hide from listTools() and block in callTool()\
88138849
* - preFn (function): Function called before each tool execution with (toolName, toolArguments)\
88148850
* - posFn (function): Function called after each tool execution with (toolName, toolArguments, result)\
88158851
* - auth (map): Optional authentication options for remote/http type:\
@@ -8952,13 +8988,27 @@ const $mcp = function(aOptions) {
89528988
name: "OpenAF MCP Client",
89538989
version: "1.0.0"
89548990
})
8991+
aOptions.blacklist = _$(aOptions.blacklist, "aOptions.blacklist").isArray().default([])
89558992
aOptions.options = _$(aOptions.options, "aOptions.options").isMap().default(__)
89568993
aOptions.auth = _$(aOptions.auth, "aOptions.auth").isMap().default({})
89578994
aOptions.preFn = _$(aOptions.preFn, "aOptions.preFn").isFunction().default(__)
89588995
aOptions.posFn = _$(aOptions.posFn, "aOptions.posFn").isFunction().default(__)
89598996
aOptions.protocolVersion = _$(aOptions.protocolVersion, "aOptions.protocolVersion").isString().default("2024-11-05")
89608997

8998+
const _toolBlacklist = {}
8999+
aOptions.blacklist.forEach(toolName => {
9000+
toolName = _$(toolName, "aOptions.blacklist[]").isString().$_()
9001+
_toolBlacklist[toolName] = true
9002+
})
9003+
89619004
const _defaultCmdDir = (isDef(__flags) && isDef(__flags.JSONRPC) && isDef(__flags.JSONRPC.cmd) && isDef(__flags.JSONRPC.cmd.defaultDir)) ? __flags.JSONRPC.cmd.defaultDir : __
9005+
const _isToolBlacklisted = toolName => _toolBlacklist[toolName] === true
9006+
const _filterToolsList = toolsRes => {
9007+
if (isMap(toolsRes) && isArray(toolsRes.tools) && Object.keys(_toolBlacklist).length > 0) {
9008+
toolsRes.tools = toolsRes.tools.filter(tool => !_isToolBlacklisted(tool.name))
9009+
}
9010+
return toolsRes
9011+
}
89629012

89639013
const _auth = {
89649014
token: __,
@@ -9283,11 +9333,11 @@ const $mcp = function(aOptions) {
92839333
}
92849334
},
92859335
getInfo: () => _r._initResult,
9286-
listTools: () => {
9336+
listTools: () => {
92879337
if (!_r._initialized) {
92889338
throw new Error("MCP client not initialized. Call initialize() first.")
92899339
}
9290-
return _execWithAuth("tools/list", {})
9340+
return _filterToolsList(_execWithAuth("tools/list", {}))
92919341
},
92929342
callTool: (toolName, toolArguments, toolOptions) => {
92939343
if (!_r._initialized) {
@@ -9296,6 +9346,9 @@ const $mcp = function(aOptions) {
92969346
toolName = _$(toolName, "toolName").isString().$_()
92979347
toolArguments = _$(toolArguments, "toolArguments").isMap().default({})
92989348
toolOptions = _$(toolOptions, "toolOptions").isMap().default(__)
9349+
if (_isToolBlacklisted(toolName)) {
9350+
throw new Error("MCP tool '" + toolName + "' is blacklisted.")
9351+
}
92999352

93009353
// Call pre-function if provided
93019354
if (aOptions.preFn) {

tests/autoTestAll.A2A.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,61 @@
216216
}
217217
};
218218

219+
exports.testClientToolBlacklist = function() {
220+
var client = $mcp({
221+
type: "dummy",
222+
blacklist: ["secret_tool"],
223+
options: {
224+
fns: {
225+
visible_tool: function(params) {
226+
return {
227+
content: [{ type: "text", text: "visible" }],
228+
isError: false
229+
};
230+
},
231+
secret_tool: function(params) {
232+
return {
233+
content: [{ type: "text", text: "secret" }],
234+
isError: false
235+
};
236+
}
237+
},
238+
fnsMeta: {
239+
visible_tool: {
240+
name: "visible_tool",
241+
description: "Visible tool",
242+
inputSchema: { type: "object", properties: {} }
243+
},
244+
secret_tool: {
245+
name: "secret_tool",
246+
description: "Secret tool",
247+
inputSchema: { type: "object", properties: {} }
248+
}
249+
}
250+
}
251+
});
252+
253+
client.initialize();
254+
255+
var tools = client.listTools();
256+
ow.test.assert(isArray(tools.tools), true, "Dummy MCP should list tools");
257+
ow.test.assert(tools.tools.length, 1, "Blacklisted tool should be excluded from listTools");
258+
ow.test.assert(tools.tools[0].name, "visible_tool", "Only non-blacklisted tool should be listed");
259+
260+
var visibleRes = client.callTool("visible_tool", {});
261+
ow.test.assert(visibleRes.content[0].text, "visible", "Non-blacklisted tool should execute");
262+
263+
var blocked = false;
264+
try {
265+
client.callTool("secret_tool", {});
266+
} catch(e) {
267+
blocked = String(e.message).indexOf("blacklisted") >= 0;
268+
}
269+
ow.test.assert(blocked, true, "Blacklisted tool should be rejected by callTool");
270+
271+
client.destroy();
272+
};
273+
219274
exports.testSendMessage = function() {
220275
ow.loadServer();
221276

tests/autoTestAll.A2A.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ jobs:
3737
exec: |
3838
args.func = args.tests.testClientRemoteSSE;
3939
40+
# ---------------------------------------------------
41+
- name: A2A::Client Tool Blacklist
42+
from: A2A::Init
43+
to : oJob Test
44+
exec: |
45+
args.func = args.tests.testClientToolBlacklist;
46+
4047
# ---------------------------------------------------
4148
- name: A2A::Send Message
4249
from: A2A::Init

0 commit comments

Comments
 (0)