Skip to content

Commit 998e0fc

Browse files
authored
[Agentic Search] Add MCP server support (#802)
Signed-off-by: Tyler Ohlsen <[email protected]>
1 parent d35addc commit 998e0fc

18 files changed

+808
-93
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
99
- Clean up / hide complex fields on agent configuration ([#796](https://github.com/opensearch-project/dashboards-flow-framework/pull/796))
1010
- Add agent summary ([#801](https://github.com/opensearch-project/dashboards-flow-framework/pull/801))
1111
- Clean up agent summary formatting ([#803](https://github.com/opensearch-project/dashboards-flow-framework/pull/803))
12+
- [Agentic Search] Add MCP server support ([#802](https://github.com/opensearch-project/dashboards-flow-framework/pull/802))
1213
### Bug Fixes
1314
### Infrastructure
1415
### Documentation

common/constants.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Agent,
88
InputMapEntry,
99
MapEntry,
10+
MCPConnector,
1011
PromptPreset,
1112
QueryPreset,
1213
WORKFLOW_STATE,
@@ -328,8 +329,10 @@ export const AGENT_MAIN_DOCS_LINK =
328329
'https://docs.opensearch.org/latest/ml-commons-plugin/agents-tools/agents/index/';
329330
export const AGENTIC_SEARCH_DOCS_LINK =
330331
'https://docs.opensearch.org/latest/vector-search/ai-search/agentic-search/';
332+
export const MCP_CONNECTOR_DOCS_LINK =
333+
'https://docs.opensearch.org/latest/ml-commons-plugin/agents-tools/mcp/mcp-connector';
331334
export const MCP_AGENT_CONFIG_DOCS_LINK =
332-
'https://docs.opensearch.org/latest/ml-commons-plugin/agents-tools/mcp/mcp-connector/#step-3-register-an-agent-for-accessing-mcp-tools';
335+
'https://docs.opensearch.org/latest/ml-commons-plugin/agents-tools/mcp/mcp-connector#step-3-register-an-agent-for-accessing-mcp-tools';
333336
export const AGENT_FIELDS_DOCS_LINK =
334337
'https://docs.opensearch.org/latest/ml-commons-plugin/api/agent-apis/register-agent/#request-body-fields';
335338
export const TOOLS_DOCS_LINK =
@@ -1054,6 +1057,13 @@ export enum AGENT_MEMORY_TYPE {
10541057
CONVERSATION_INDEX = 'conversation_index',
10551058
}
10561059

1060+
export enum CONNECTOR_PROTOCOL {
1061+
AWS_SIGV4 = 'aws_sigv4',
1062+
HTTP = 'http',
1063+
MCP_SSE = 'mcp_sse',
1064+
MCP_STREAMABLE_HTTP = 'mcp_streamable_http',
1065+
}
1066+
10571067
export enum AGENT_LLM_INTERFACE_TYPE {
10581068
OPENAI = 'openai/v1/chat/completions',
10591069
BEDROCK_CLAUDE = 'bedrock/converse/claude',
@@ -1089,3 +1099,8 @@ export const DEFAULT_AGENT = {
10891099
type: AGENT_MEMORY_TYPE.CONVERSATION_INDEX,
10901100
},
10911101
} as Partial<Agent>;
1102+
1103+
export const DEFAULT_MCP_SERVER = {
1104+
mcp_connector_id: '',
1105+
tool_filters: [],
1106+
} as MCPConnector;

common/interfaces.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
AGENT_MEMORY_TYPE,
1212
AGENT_TYPE,
1313
COMPONENT_CLASS,
14+
CONNECTOR_PROTOCOL,
1415
PROCESSOR_TYPE,
1516
TOOL_TYPE,
1617
TRANSFORM_TYPE,
@@ -495,6 +496,7 @@ export type Model = {
495496
export type Connector = {
496497
id: string;
497498
name: string;
499+
protocol?: CONNECTOR_PROTOCOL;
498500
parameters?: ConnectorParameters;
499501
actions: {}[];
500502
client_config?: {};
@@ -508,8 +510,14 @@ export type ConnectorDict = {
508510
[connectorId: string]: Connector;
509511
};
510512

513+
export type MCPConnector = {
514+
mcp_connector_id: string;
515+
tool_filters: string[];
516+
};
517+
511518
export type AgentConfigParameters = {
512519
_llm_interface?: AGENT_LLM_INTERFACE_TYPE;
520+
mcp_connectors?: MCPConnector[];
513521
[key: string]: any;
514522
};
515523

public/pages/workflow_detail/agentic_search/configure_flow/agent_configuration.tsx

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,18 @@ import {
3737
EMPTY_AGENT,
3838
MAX_DESCRIPTION_LENGTH,
3939
MAX_STRING_LENGTH,
40+
MCP_CONNECTOR_DOCS_LINK,
4041
NEW_AGENT_ID_PLACEHOLDER,
4142
NEW_AGENT_PLACEHOLDER,
4243
WorkflowConfig,
4344
WorkflowFormValues,
4445
} from '../../../../../common';
4546
import { AppState } from '../../../../store';
47+
import { sanitizeJSON } from '../../../../utils';
4648
import { AgentTools } from './agent_tools';
4749
import { SimplifiedJsonField } from '../components';
4850
import { AgentLLMFields } from './agent_llm_fields';
49-
import { sanitizeJSON } from '../../../../utils';
51+
import { AgentMCPServers } from './agent_mcp_servers';
5052
import { AgentAdvancedSettings } from './agent_advanced_settings';
5153

5254
interface AgentConfigurationProps {
@@ -403,16 +405,13 @@ export function AgentConfiguration(props: AgentConfigurationProps) {
403405
}
404406
placeholder="Enter description"
405407
aria-label="Enter description"
406-
rows={2}
408+
rows={1}
407409
fullWidth
408410
compressed
409411
maxLength={MAX_DESCRIPTION_LENGTH}
410412
/>
411413
</EuiFormRow>
412414
</EuiFlexItem>
413-
{/**
414-
* Show the agent model dropdown if applicable
415-
*/}
416415
<EuiFlexItem grow={false}>
417416
<AgentLLMFields
418417
agentType={agentType as AGENT_TYPE}
@@ -428,6 +427,29 @@ export function AgentConfiguration(props: AgentConfigurationProps) {
428427
/>
429428
</EuiFormRow>
430429
</EuiFlexItem>
430+
{agentType === AGENT_TYPE.CONVERSATIONAL && (
431+
<EuiFlexItem grow={false}>
432+
<EuiFormRow
433+
label="MCP servers"
434+
labelAppend={
435+
<EuiText size="xs">
436+
<EuiLink
437+
href={MCP_CONNECTOR_DOCS_LINK}
438+
target="_blank"
439+
>
440+
Learn more
441+
</EuiLink>
442+
</EuiText>
443+
}
444+
fullWidth
445+
>
446+
<AgentMCPServers
447+
agentForm={props.agentForm}
448+
setAgentForm={props.setAgentForm}
449+
/>
450+
</EuiFormRow>
451+
</EuiFlexItem>
452+
)}
431453
<EuiFlexItem grow={false}>
432454
<AgentAdvancedSettings
433455
agentForm={props.agentForm}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from 'react';
7+
import { render, screen, fireEvent } from '@testing-library/react';
8+
import '@testing-library/jest-dom';
9+
import { Provider } from 'react-redux';
10+
import configureStore from 'redux-mock-store';
11+
import { AgentMCPServers } from './agent_mcp_servers';
12+
import { CONNECTOR_PROTOCOL, DEFAULT_MCP_SERVER } from '../../../../../common';
13+
14+
// Setup mock store
15+
const mockStore = configureStore([]);
16+
17+
describe('AgentMCPServers', () => {
18+
// Initial state for the Redux store
19+
const initialState = {
20+
ml: {
21+
connectors: {
22+
'connector-1': {
23+
id: 'connector-1',
24+
name: 'Test Connector 1',
25+
protocol: CONNECTOR_PROTOCOL.MCP_SSE,
26+
},
27+
'connector-2': {
28+
id: 'connector-2',
29+
name: 'Test Connector 2',
30+
protocol: CONNECTOR_PROTOCOL.MCP_STREAMABLE_HTTP,
31+
},
32+
},
33+
},
34+
};
35+
36+
const mockSetAgentForm = jest.fn();
37+
38+
beforeEach(() => {
39+
jest.clearAllMocks();
40+
});
41+
42+
test('renders with no MCP servers initially', () => {
43+
const store = mockStore(initialState);
44+
const mockAgentForm = {
45+
parameters: {
46+
mcp_connectors: [],
47+
},
48+
};
49+
50+
render(
51+
<Provider store={store}>
52+
<AgentMCPServers
53+
agentForm={mockAgentForm}
54+
setAgentForm={mockSetAgentForm}
55+
/>
56+
</Provider>
57+
);
58+
59+
// Check that the "Add MCP server" button is present
60+
expect(screen.getByTestId('addMCPServerButton')).toBeInTheDocument();
61+
});
62+
63+
test('adds a new MCP server when "Add MCP server" is clicked', () => {
64+
const store = mockStore(initialState);
65+
const mockAgentForm = {
66+
parameters: {
67+
mcp_connectors: [],
68+
},
69+
};
70+
71+
render(
72+
<Provider store={store}>
73+
<AgentMCPServers
74+
agentForm={mockAgentForm}
75+
setAgentForm={mockSetAgentForm}
76+
/>
77+
</Provider>
78+
);
79+
80+
fireEvent.click(screen.getByText('Add MCP server'));
81+
82+
// Check if setAgentForm is called with the correct arguments
83+
expect(mockSetAgentForm).toHaveBeenCalledWith({
84+
parameters: {
85+
mcp_connectors: [DEFAULT_MCP_SERVER],
86+
},
87+
});
88+
});
89+
90+
test('removes an MCP server when trash button is clicked', () => {
91+
const store = mockStore(initialState);
92+
const mockAgentForm = {
93+
parameters: {
94+
mcp_connectors: [
95+
{
96+
mcp_connector_id: 'connector-1',
97+
tool_filters: ['filter1', 'filter2'],
98+
},
99+
],
100+
},
101+
};
102+
103+
render(
104+
<Provider store={store}>
105+
<AgentMCPServers
106+
agentForm={mockAgentForm}
107+
setAgentForm={mockSetAgentForm}
108+
/>
109+
</Provider>
110+
);
111+
112+
// Find and click the trash button
113+
const trashButton = screen.getByTestId('removeMCPServerButton');
114+
fireEvent.click(trashButton);
115+
116+
// Check if setAgentForm is called with the correct arguments (empty array)
117+
expect(mockSetAgentForm).toHaveBeenCalledWith({
118+
parameters: {
119+
mcp_connectors: [],
120+
},
121+
});
122+
});
123+
124+
test('disables "Add MCP server" button when there is an empty connector ID', () => {
125+
const store = mockStore(initialState);
126+
const mockAgentForm = {
127+
parameters: {
128+
mcp_connectors: [
129+
{
130+
mcp_connector_id: '', // Empty connector ID
131+
tool_filters: [],
132+
},
133+
],
134+
},
135+
};
136+
137+
render(
138+
<Provider store={store}>
139+
<AgentMCPServers
140+
agentForm={mockAgentForm}
141+
setAgentForm={mockSetAgentForm}
142+
/>
143+
</Provider>
144+
);
145+
146+
// Get the add button and check its disabled property
147+
const addButton = screen.getByTestId('addMCPServerButton');
148+
expect(addButton).toBeDisabled();
149+
});
150+
});

0 commit comments

Comments
 (0)