Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
change
sglang
Commits
7fb551a7
Unverified
Commit
7fb551a7
authored
Sep 30, 2025
by
Keyang Ru
Committed by
GitHub
Sep 30, 2025
Browse files
[router] add mcp list and mcp call in output array (#11112)
parent
1193f131
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
216 additions
and
1 deletion
+216
-1
sgl-router/py_test/e2e/test_regular_router.py
sgl-router/py_test/e2e/test_regular_router.py
+1
-1
sgl-router/src/protocols/spec.rs
sgl-router/src/protocols/spec.rs
+30
-0
sgl-router/src/routers/http/openai_router.rs
sgl-router/src/routers/http/openai_router.rs
+134
-0
sgl-router/tests/responses_api_test.rs
sgl-router/tests/responses_api_test.rs
+51
-0
No files found.
sgl-router/py_test/e2e/test_regular_router.py
View file @
7fb551a7
...
@@ -45,7 +45,7 @@ def test_genai_bench(
...
@@ -45,7 +45,7 @@ def test_genai_bench(
thresholds
=
{
thresholds
=
{
"ttft_mean_max"
:
6
,
"ttft_mean_max"
:
6
,
"e2e_latency_mean_max"
:
14
,
"e2e_latency_mean_max"
:
14
,
"input_throughput_mean_min"
:
1000
,
"input_throughput_mean_min"
:
800
,
# temp relax from 1000 to 800 for now
"output_throughput_mean_min"
:
12
,
"output_throughput_mean_min"
:
12
,
# Enforce GPU utilization p50 >= 99% during the run.
# Enforce GPU utilization p50 >= 99% during the run.
"gpu_util_p50_min"
:
99
,
"gpu_util_p50_min"
:
99
,
...
...
sgl-router/src/protocols/spec.rs
View file @
7fb551a7
...
@@ -797,6 +797,17 @@ pub enum ResponseReasoningContent {
...
@@ -797,6 +797,17 @@ pub enum ResponseReasoningContent {
ReasoningText
{
text
:
String
},
ReasoningText
{
text
:
String
},
}
}
/// MCP Tool information for the mcp_list_tools output item
#[derive(Debug,
Clone,
Deserialize,
Serialize)]
pub
struct
McpToolInfo
{
pub
name
:
String
,
#[serde(skip_serializing_if
=
"Option::is_none"
)]
pub
description
:
Option
<
String
>
,
pub
input_schema
:
Value
,
#[serde(skip_serializing_if
=
"Option::is_none"
)]
pub
annotations
:
Option
<
Value
>
,
}
#[derive(Debug,
Clone,
Deserialize,
Serialize)]
#[derive(Debug,
Clone,
Deserialize,
Serialize)]
#[serde(tag
=
"type"
)]
#[serde(tag
=
"type"
)]
#[serde(rename_all
=
"snake_case"
)]
#[serde(rename_all
=
"snake_case"
)]
...
@@ -826,6 +837,25 @@ pub enum ResponseOutputItem {
...
@@ -826,6 +837,25 @@ pub enum ResponseOutputItem {
output
:
Option
<
String
>
,
output
:
Option
<
String
>
,
status
:
String
,
status
:
String
,
},
},
#[serde(rename
=
"mcp_list_tools"
)]
McpListTools
{
id
:
String
,
server_label
:
String
,
tools
:
Vec
<
McpToolInfo
>
,
},
#[serde(rename
=
"mcp_call"
)]
McpCall
{
id
:
String
,
status
:
String
,
#[serde(skip_serializing_if
=
"Option::is_none"
)]
approval_request_id
:
Option
<
String
>
,
arguments
:
String
,
#[serde(skip_serializing_if
=
"Option::is_none"
)]
error
:
Option
<
String
>
,
name
:
String
,
output
:
String
,
server_label
:
String
,
},
}
}
#[derive(Debug,
Clone,
Deserialize,
Serialize)]
#[derive(Debug,
Clone,
Deserialize,
Serialize)]
...
...
sgl-router/src/routers/http/openai_router.rs
View file @
7fb551a7
...
@@ -449,6 +449,32 @@ impl OpenAIRouter {
...
@@ -449,6 +449,32 @@ impl OpenAIRouter {
.await
.await
{
{
Ok
(
mut
resumed_json
)
=>
{
Ok
(
mut
resumed_json
)
=>
{
// Inject MCP output items (mcp_list_tools and mcp_call)
let
server_label
=
original_body
.tools
.iter
()
.find
(|
t
|
matches!
(
t
.r
#
type
,
ResponseToolType
::
Mcp
))
.and_then
(|
t
|
t
.server_label
.as_deref
())
.unwrap_or
(
"mcp"
);
if
let
Err
(
inject_err
)
=
Self
::
inject_mcp_output_items
(
&
mut
resumed_json
,
mcp
,
McpOutputItemsArgs
{
tool_name
:
&
tool_name
,
args_json
:
&
args_json_str
,
output
:
&
output_payload
,
server_label
,
success
:
call_ok
,
error
:
call_error
.as_deref
(),
},
)
{
warn!
(
"Failed to inject MCP output items: {}"
,
inject_err
);
}
if
!
call_ok
{
if
!
call_ok
{
if
let
Some
(
obj
)
=
resumed_json
.as_object_mut
()
{
if
let
Some
(
obj
)
=
resumed_json
.as_object_mut
()
{
let
metadata_value
=
let
metadata_value
=
...
@@ -1025,6 +1051,15 @@ struct ResumeWithToolArgs<'a> {
...
@@ -1025,6 +1051,15 @@ struct ResumeWithToolArgs<'a> {
original_body
:
&
'a
ResponsesRequest
,
original_body
:
&
'a
ResponsesRequest
,
}
}
struct
McpOutputItemsArgs
<
'a
>
{
tool_name
:
&
'a
str
,
args_json
:
&
'a
str
,
output
:
&
'a
str
,
server_label
:
&
'a
str
,
success
:
bool
,
error
:
Option
<&
'a
str
>
,
}
impl
OpenAIRouter
{
impl
OpenAIRouter
{
fn
extract_function_call
(
resp
:
&
Value
)
->
Option
<
(
String
,
String
,
String
)
>
{
fn
extract_function_call
(
resp
:
&
Value
)
->
Option
<
(
String
,
String
,
String
)
>
{
let
output
=
resp
.get
(
"output"
)
?
.as_array
()
?
;
let
output
=
resp
.get
(
"output"
)
?
.as_array
()
?
;
...
@@ -1115,6 +1150,105 @@ impl OpenAIRouter {
...
@@ -1115,6 +1150,105 @@ impl OpenAIRouter {
Ok
((
server_name
,
output_str
))
Ok
((
server_name
,
output_str
))
}
}
/// Generate a unique ID for MCP output items (similar to OpenAI format)
fn
generate_mcp_id
(
prefix
:
&
str
)
->
String
{
use
rand
::
RngCore
;
let
mut
rng
=
rand
::
rng
();
let
mut
bytes
=
[
0u8
;
30
];
rng
.fill_bytes
(
&
mut
bytes
);
let
hex_string
:
String
=
bytes
.iter
()
.map
(|
b
|
format!
(
"{:02x}"
,
b
))
.collect
();
format!
(
"{}_{}"
,
prefix
,
hex_string
)
}
/// Build an mcp_list_tools output item
fn
build_mcp_list_tools_item
(
mcp
:
&
Arc
<
crate
::
mcp
::
McpClientManager
>
,
server_label
:
&
str
,
)
->
Value
{
let
tools
=
mcp
.list_tools
();
let
tools_json
:
Vec
<
Value
>
=
tools
.iter
()
.map
(|
t
|
{
json!
({
"name"
:
t
.name
,
"description"
:
t
.description
,
"input_schema"
:
t
.parameters
.clone
()
.unwrap_or_else
(||
json!
({
"type"
:
"object"
,
"properties"
:
{},
"additionalProperties"
:
false
})),
"annotations"
:
{
"read_only"
:
false
}
})
})
.collect
();
json!
({
"id"
:
Self
::
generate_mcp_id
(
"mcpl"
),
"type"
:
"mcp_list_tools"
,
"server_label"
:
server_label
,
"tools"
:
tools_json
})
}
/// Build an mcp_call output item
fn
build_mcp_call_item
(
tool_name
:
&
str
,
arguments
:
&
str
,
output
:
&
str
,
server_label
:
&
str
,
success
:
bool
,
error
:
Option
<&
str
>
,
)
->
Value
{
json!
({
"id"
:
Self
::
generate_mcp_id
(
"mcp"
),
"type"
:
"mcp_call"
,
"status"
:
if
success
{
"completed"
}
else
{
"failed"
},
"approval_request_id"
:
Value
::
Null
,
"arguments"
:
arguments
,
"error"
:
error
,
"name"
:
tool_name
,
"output"
:
output
,
"server_label"
:
server_label
})
}
/// Inject mcp_list_tools and mcp_call items into the response output array
fn
inject_mcp_output_items
(
response_json
:
&
mut
Value
,
mcp
:
&
Arc
<
crate
::
mcp
::
McpClientManager
>
,
args
:
McpOutputItemsArgs
,
)
->
Result
<
(),
String
>
{
let
output_array
=
response_json
.get_mut
(
"output"
)
.and_then
(|
v
|
v
.as_array_mut
())
.ok_or
(
"missing output array"
)
?
;
// Build MCP output items
let
list_tools_item
=
Self
::
build_mcp_list_tools_item
(
mcp
,
args
.server_label
);
let
call_item
=
Self
::
build_mcp_call_item
(
args
.tool_name
,
args
.args_json
,
args
.output
,
args
.server_label
,
args
.success
,
args
.error
,
);
// Find the index of the last message item to insert mcp_call before it
let
call_insertion_index
=
output_array
.iter
()
.rposition
(|
item
|
item
.get
(
"type"
)
.and_then
(|
v
|
v
.as_str
())
==
Some
(
"message"
))
.unwrap_or
(
output_array
.len
());
// Insert items in-place for efficiency
output_array
.insert
(
call_insertion_index
,
call_item
);
output_array
.insert
(
0
,
list_tools_item
);
Ok
(())
}
async
fn
resume_with_tool_result
(
&
self
,
args
:
ResumeWithToolArgs
<
'_
>
)
->
Result
<
Value
,
String
>
{
async
fn
resume_with_tool_result
(
&
self
,
args
:
ResumeWithToolArgs
<
'_
>
)
->
Result
<
Value
,
String
>
{
let
mut
payload2
=
args
.original_payload
.clone
();
let
mut
payload2
=
args
.original_payload
.clone
();
let
obj
=
payload2
let
obj
=
payload2
...
...
sgl-router/tests/responses_api_test.rs
View file @
7fb551a7
...
@@ -143,6 +143,57 @@ async fn test_non_streaming_mcp_minimal_e2e_with_persistence() {
...
@@ -143,6 +143,57 @@ async fn test_non_streaming_mcp_minimal_e2e_with_persistence() {
.expect
(
"response output missing"
);
.expect
(
"response output missing"
);
assert
!
(
!
output
.is_empty
(),
"expected at least one output item"
);
assert
!
(
!
output
.is_empty
(),
"expected at least one output item"
);
// Verify mcp_list_tools item is present
let
list_tools_item
=
output
.iter
()
.find
(|
entry
|
{
entry
.get
(
"type"
)
==
Some
(
&
serde_json
::
Value
::
String
(
"mcp_list_tools"
.into
()))
})
.expect
(
"missing mcp_list_tools output item"
);
assert_eq!
(
list_tools_item
.get
(
"server_label"
)
.and_then
(|
v
|
v
.as_str
()),
Some
(
"mock"
),
"server_label should match"
);
let
tools_list
=
list_tools_item
.get
(
"tools"
)
.and_then
(|
v
|
v
.as_array
())
.expect
(
"tools array missing in mcp_list_tools"
);
assert
!
(
!
tools_list
.is_empty
(),
"mcp_list_tools should contain at least one tool"
);
// Verify mcp_call item is present
let
mcp_call_item
=
output
.iter
()
.find
(|
entry
|
entry
.get
(
"type"
)
==
Some
(
&
serde_json
::
Value
::
String
(
"mcp_call"
.into
())))
.expect
(
"missing mcp_call output item"
);
assert_eq!
(
mcp_call_item
.get
(
"status"
)
.and_then
(|
v
|
v
.as_str
()),
Some
(
"completed"
),
"mcp_call status should be completed"
);
assert_eq!
(
mcp_call_item
.get
(
"server_label"
)
.and_then
(|
v
|
v
.as_str
()),
Some
(
"mock"
),
"server_label should match"
);
assert
!
(
mcp_call_item
.get
(
"name"
)
.is_some
(),
"mcp_call should have a tool name"
);
assert
!
(
mcp_call_item
.get
(
"arguments"
)
.is_some
(),
"mcp_call should have arguments"
);
assert
!
(
mcp_call_item
.get
(
"output"
)
.is_some
(),
"mcp_call should have output"
);
let
final_text
=
output
let
final_text
=
output
.iter
()
.iter
()
.rev
()
.rev
()
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment