Current section
Files
Jump to
Current section
Files
src/mcp_server_config.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% MCP Server Configuration Module
%%% Manages remote MCP (Model Context Protocol) servers and their capabilities
%%% Enhanced with comprehensive logging and error reporting
%%% @end
%%%-------------------------------------------------------------------
-module(mcp_server_config).
-export([init/0, get_seed_servers/0, get_all_servers/0, add_server/1,
update_server/2, delete_server/1, get_server/1,
discover_capabilities/1, get_server_capabilities/1,
assign_server_to_agent/2, get_agent_servers/1]).
-include_lib("stdlib/include/qlc.hrl").
%% Enhanced logging utility with line numbers
-define(LOG(Level, Format, Args),
io:format("[~s:~p] [~s] " ++ Format ++ "~n",
[?MODULE, ?LINE, Level | Args])).
-define(LOG_INFO(Format, Args), ?LOG("INFO", Format, Args)).
-define(LOG_WARN(Format, Args), ?LOG("WARN", Format, Args)).
-define(LOG_ERROR(Format, Args), ?LOG("ERROR", Format, Args)).
-define(LOG_DEBUG(Format, Args), ?LOG("DEBUG", Format, Args)).
-record(mcp_server, {
id :: binary(),
name :: binary(),
category :: binary(),
url :: binary(),
auth_type :: oauth2 | api_key | open,
maintainer :: binary(),
description :: binary(),
capabilities :: list(),
status :: active | inactive,
last_checked :: calendar:datetime(),
metadata :: map()
}).
-record(agent_mcp_mapping, {
agent_id :: binary(),
server_ids :: [binary()]
}).
%%%===================================================================
%%% API
%%%===================================================================
init() ->
?LOG_INFO("Initializing MCP server configuration", []),
% Ensure Mnesia is started first
case mnesia:start() of
ok ->
?LOG_INFO("Mnesia started successfully", []);
{error, {already_started, mnesia}} ->
?LOG_INFO("Mnesia already started", []);
{error, Reason} ->
?LOG_ERROR("Failed to start Mnesia: ~p", [Reason]),
error(Reason)
end,
% Wait for Mnesia tables to be ready
?LOG_DEBUG("Waiting for Mnesia schema tables", []),
mnesia:wait_for_tables([schema], 5000),
% Create tables if they don't exist
% Use ram_copies for development when no proper node name is set
TableType = case node() of
nonode@nohost -> ram_copies;
_ -> disc_copies
end,
?LOG_INFO("Using table type: ~p for node: ~p", [TableType, node()]),
case mnesia:create_table(mcp_server,
[{attributes, record_info(fields, mcp_server)},
{TableType, [node()]}]) of
{atomic, ok} ->
?LOG_INFO("Created mcp_server table", []);
{aborted, {already_exists, mcp_server}} ->
?LOG_INFO("mcp_server table already exists", []);
{aborted, Reason1} ->
?LOG_ERROR("Failed to create mcp_server table: ~p", [Reason1])
end,
case mnesia:create_table(agent_mcp_mapping,
[{attributes, record_info(fields, agent_mcp_mapping)},
{TableType, [node()]}]) of
{atomic, ok} ->
?LOG_INFO("Created agent_mcp_mapping table", []);
{aborted, {already_exists, agent_mcp_mapping}} ->
?LOG_INFO("agent_mcp_mapping table already exists", []);
{aborted, Reason2} ->
?LOG_ERROR("Failed to create agent_mcp_mapping table: ~p", [Reason2])
end,
% Load seed servers
?LOG_INFO("Loading seed servers", []),
load_seed_servers(),
?LOG_INFO("MCP server configuration initialization complete", []).
get_seed_servers() ->
[
#{id => <<"asana">>,
name => <<"Asana">>,
category => <<"Project Management">>,
url => <<"https://mcp.asana.com/sse">>,
auth_type => oauth2,
maintainer => <<"Asana">>,
description => <<"Interact with your Asana workspace through AI tools to keep projects on track">>,
capabilities => [
<<"task_management">>, <<"project_tracking">>, <<"team_collaboration">>
],
metadata => #{
official => true,
documentation_url => <<"https://developers.asana.com/mcp">>,
requires_auth => true
}
},
#{id => <<"atlassian">>,
name => <<"Atlassian">>,
category => <<"Software Development">>,
url => <<"https://mcp.atlassian.com/v1/sse">>,
auth_type => oauth2,
maintainer => <<"Atlassian">>,
description => <<"Access Atlassian services including Jira and Confluence">>},
#{id => <<"linear">>,
name => <<"Linear">>,
category => <<"Project Management">>,
url => <<"https://mcp.linear.app/sse">>,
auth_type => oauth2,
maintainer => <<"Linear">>,
description => <<"Issue tracking and project management for software teams">>},
#{id => <<"intercom">>,
name => <<"Intercom">>,
category => <<"Customer Support">>,
url => <<"https://mcp.intercom.com/sse">>,
auth_type => oauth2,
maintainer => <<"Intercom">>,
description => <<"Access real-time customer conversations, tickets, and user data from Intercom">>,
capabilities => [
<<"conversation_management">>, <<"ticket_handling">>, <<"user_data_access">>
],
metadata => #{
official => true,
documentation_url => <<"https://developers.intercom.com/mcp">>,
requires_auth => true
}
},
#{id => <<"invideo">>,
name => <<"invideo">>,
category => <<"Media Creation">>,
url => <<"https://mcp.invideo.io/sse">>,
auth_type => api_key,
maintainer => <<"invideo">>,
description => <<"Build video creation capabilities into your applications">>,
capabilities => [
<<"video_creation">>, <<"template_management">>, <<"media_processing">>
],
metadata => #{
official => true,
documentation_url => <<"https://developer.invideo.io/mcp">>,
requires_auth => true
}
},
#{id => <<"paypal">>,
name => <<"PayPal">>,
category => <<"Finance">>,
url => <<"https://mcp.paypal.com/sse">>,
auth_type => oauth2,
maintainer => <<"PayPal">>,
description => <<"Integrate PayPal commerce capabilities">>,
capabilities => [
<<"payment_processing">>, <<"transaction_management">>, <<"merchant_services">>
],
metadata => #{
official => true,
documentation_url => <<"https://developer.paypal.com/mcp">>,
requires_auth => true
}
},
#{id => <<"plaid">>,
name => <<"Plaid">>,
category => <<"Finance">>,
url => <<"https://api.dashboard.plaid.com/mcp/sse">>,
auth_type => api_key,
maintainer => <<"Plaid">>,
description => <<"Analyze, troubleshoot, and optimize Plaid integrations">>,
capabilities => [
<<"banking_data">>, <<"transaction_analysis">>, <<"financial_insights">>
],
metadata => #{
official => true,
documentation_url => <<"https://plaid.com/docs/mcp">>,
requires_auth => true
}
},
#{id => <<"square">>,
name => <<"Square">>,
category => <<"Commerce">>,
url => <<"https://mcp.squareup.com/sse">>,
auth_type => oauth2,
maintainer => <<"Square">>,
description => <<"Use an agent to build on Square APIs. Payments, inventory, orders, and more">>,
capabilities => [
<<"payment_processing">>, <<"inventory_management">>, <<"order_management">>
],
metadata => #{
official => true,
documentation_url => <<"https://developer.squareup.com/mcp">>,
requires_auth => true
}
},
#{id => <<"workato">>,
name => <<"Workato">>,
category => <<"Automation">>,
url => <<"https://mcp.workato.com/sse">>,
auth_type => oauth2,
maintainer => <<"Workato">>,
description => <<"Access any application, workflows or data via Workato, made accessible for AI">>,
capabilities => [
<<"workflow_automation">>, <<"data_integration">>, <<"app_connectivity">>
],
metadata => #{
official => true,
documentation_url => <<"https://docs.workato.com/mcp">>,
requires_auth => true,
programmatic_generation => true
}
},
#{id => <<"zapier">>,
name => <<"Zapier">>,
category => <<"Automation">>,
url => <<"https://mcp.zapier.com/">>,
auth_type => oauth2,
maintainer => <<"Zapier">>,
description => <<"Connect to nearly 8,000 apps through Zapier's automation platform">>,
capabilities => [
<<"app_integration">>, <<"workflow_automation">>, <<"trigger_management">>
],
metadata => #{
official => true,
documentation_url => <<"https://zapier.com/developer/mcp">>,
requires_auth => true,
supported_apps => 8000
}
},
#{id => <<"graphlit">>,
name => <<"Graphlit">>,
category => <<"Knowledge Management">>,
url => <<"npx -y graphlit-mcp-server">>,
auth_type => api_key,
maintainer => <<"Graphlit">>,
description => <<"Ingest anything from Slack, Discord, websites, Google Drive, email, Jira, Linear or GitHub into a searchable, RAG-ready knowledge base. Supports web crawling, search, and retrieval with no additional tools needed.">>,
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"graphlit-mcp-server">>],
<<"required_env">> => [<<"GRAPHLIT_ORGANIZATION_ID">>, <<"GRAPHLIT_ENVIRONMENT_ID">>, <<"GRAPHLIT_JWT_SECRET">>],
<<"optional_env">> => [
<<"SLACK_BOT_TOKEN">>, <<"DISCORD_BOT_TOKEN">>, <<"TWITTER_TOKEN">>,
<<"GOOGLE_EMAIL_REFRESH_TOKEN">>, <<"GOOGLE_EMAIL_CLIENT_ID">>, <<"GOOGLE_EMAIL_CLIENT_SECRET">>,
<<"LINEAR_API_KEY">>, <<"GITHUB_PERSONAL_ACCESS_TOKEN">>,
<<"JIRA_EMAIL">>, <<"JIRA_TOKEN">>, <<"NOTION_API_KEY">>
],
<<"api_url">> => <<"https://data-scus.graphlit.io/api/v1/graphql">>,
<<"organization_id">> => <<"1b591b0d-eb12-4f6d-be5a-ceb95b40e716">>,
<<"environment_id">> => <<"42d0e0ef-8eeb-463d-921c-ef5119541eb9">>,
<<"capabilities">> => [
<<"query_contents">>, <<"query_collections">>, <<"query_feeds">>,
<<"retrieve_relevant_sources">>, <<"extract_structured_json">>,
<<"ingest_files">>, <<"ingest_web_pages">>, <<"web_crawling">>,
<<"web_search">>, <<"slack_integration">>, <<"discord_integration">>,
<<"github_integration">>, <<"notion_integration">>, <<"linear_integration">>
]
}},
#{id => <<"github_mcp">>,
name => <<"GitHub MCP">>,
category => <<"Software Development">>,
url => <<"npx -y @modelcontextprotocol/server-github">>,
auth_type => api_key,
maintainer => <<"Model Context Protocol">>,
description => <<"Official GitHub MCP server for repository management, issue tracking, and code collaboration">>,
capabilities => [
<<"repository_access">>, <<"issue_management">>, <<"pull_requests">>, <<"code_search">>
],
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"@modelcontextprotocol/server-github">>],
<<"required_env">> => [<<"GITHUB_PERSONAL_ACCESS_TOKEN">>],
<<"transport">> => <<"stdio">>,
<<"official">> => true,
<<"documentation_url">> => <<"https://github.com/modelcontextprotocol/servers/tree/main/src/github">>
}},
#{id => <<"filesystem_mcp">>,
name => <<"Filesystem MCP">>,
category => <<"File Management">>,
url => <<"npx -y @modelcontextprotocol/server-filesystem">>,
auth_type => open,
maintainer => <<"Model Context Protocol">>,
description => <<"Official MCP server for safe filesystem operations with configurable access controls">>,
capabilities => [
<<"file_read">>, <<"file_write">>, <<"directory_listing">>, <<"file_search">>
],
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"@modelcontextprotocol/server-filesystem">>, <<"/path/to/allowed/directory">>],
<<"transport">> => <<"stdio">>,
<<"official">> => true,
<<"documentation_url">> => <<"https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem">>
}},
#{id => <<"postgres_mcp">>,
name => <<"PostgreSQL MCP">>,
category => <<"Database">>,
url => <<"npx -y @modelcontextprotocol/server-postgres">>,
auth_type => api_key,
maintainer => <<"Model Context Protocol">>,
description => <<"Official PostgreSQL MCP server for database operations and schema management">>,
capabilities => [
<<"sql_queries">>, <<"schema_inspection">>, <<"table_operations">>, <<"data_analysis">>
],
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"@modelcontextprotocol/server-postgres">>],
<<"required_env">> => [<<"POSTGRES_CONNECTION_STRING">>],
<<"transport">> => <<"stdio">>,
<<"official">> => true,
<<"documentation_url">> => <<"https://github.com/modelcontextprotocol/servers/tree/main/src/postgres">>
}},
#{id => <<"slack_mcp">>,
name => <<"Slack MCP">>,
category => <<"Communication">>,
url => <<"npx -y @modelcontextprotocol/server-slack">>,
auth_type => oauth2,
maintainer => <<"Model Context Protocol">>,
description => <<"Official Slack MCP server for team communication and workspace management">>,
capabilities => [
<<"message_sending">>, <<"channel_management">>, <<"user_lookup">>, <<"file_sharing">>
],
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"@modelcontextprotocol/server-slack">>],
<<"required_env">> => [<<"SLACK_BOT_TOKEN">>, <<"SLACK_TEAM_ID">>],
<<"transport">> => <<"stdio">>,
<<"official">> => true,
<<"documentation_url">> => <<"https://github.com/modelcontextprotocol/servers/tree/main/src/slack">>
}},
#{id => <<"brave_search_mcp">>,
name => <<"Brave Search MCP">>,
category => <<"Search">>,
url => <<"npx -y @modelcontextprotocol/server-brave-search">>,
auth_type => api_key,
maintainer => <<"Model Context Protocol">>,
description => <<"Official Brave Search MCP server for web search and information retrieval">>,
capabilities => [
<<"web_search">>, <<"news_search">>, <<"image_search">>, <<"video_search">>
],
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"@modelcontextprotocol/server-brave-search">>],
<<"required_env">> => [<<"BRAVE_API_KEY">>],
<<"transport">> => <<"stdio">>,
<<"official">> => true,
<<"documentation_url">> => <<"https://github.com/modelcontextprotocol/servers/tree/main/src/brave-search">>
}},
#{id => <<"puppeteer_mcp">>,
name => <<"Puppeteer MCP">>,
category => <<"Web Automation">>,
url => <<"npx -y @modelcontextprotocol/server-puppeteer">>,
auth_type => open,
maintainer => <<"Model Context Protocol">>,
description => <<"Official Puppeteer MCP server for web scraping and browser automation">>,
capabilities => [
<<"web_scraping">>, <<"screenshot_capture">>, <<"pdf_generation">>, <<"form_interaction">>
],
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"@modelcontextprotocol/server-puppeteer">>],
<<"transport">> => <<"stdio">>,
<<"official">> => true,
<<"documentation_url">> => <<"https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer">>
}},
#{id => <<"memory_mcp">>,
name => <<"Memory MCP">>,
category => <<"Data Storage">>,
url => <<"npx -y @modelcontextprotocol/server-memory">>,
auth_type => open,
maintainer => <<"Model Context Protocol">>,
description => <<"Official Memory MCP server for persistent data storage and retrieval">>,
capabilities => [
<<"data_storage">>, <<"knowledge_base">>, <<"vector_search">>, <<"semantic_retrieval">>
],
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"@modelcontextprotocol/server-memory">>],
<<"transport">> => <<"stdio">>,
<<"official">> => true,
<<"documentation_url">> => <<"https://github.com/modelcontextprotocol/servers/tree/main/src/memory">>
}},
#{id => <<"fetch_mcp">>,
name => <<"Fetch MCP">>,
category => <<"HTTP Client">>,
url => <<"npx -y @modelcontextprotocol/server-fetch">>,
auth_type => open,
maintainer => <<"Model Context Protocol">>,
description => <<"Official Fetch MCP server for HTTP requests and API interactions">>,
capabilities => [
<<"http_requests">>, <<"api_calls">>, <<"data_fetching">>, <<"webhook_handling">>
],
metadata => #{
<<"command">> => <<"npx">>,
<<"args">> => [<<"-y">>, <<"@modelcontextprotocol/server-fetch">>],
<<"transport">> => <<"stdio">>,
<<"official">> => true,
<<"documentation_url">> => <<"https://github.com/modelcontextprotocol/servers/tree/main/src/fetch">>
}}
].
get_all_servers() ->
F = fun() ->
qlc:e(qlc:q([Server || Server <- mnesia:table(mcp_server)]))
end,
case mnesia:transaction(F) of
{atomic, Servers} -> {ok, Servers};
{aborted, Reason} -> {error, Reason}
end.
add_server(ServerMap) ->
ServerId = maps:get(id, ServerMap, <<"unknown">>),
ServerName = maps:get(name, ServerMap, <<"Unnamed Server">>),
?LOG_INFO("Adding server: ~s (~s)", [ServerName, ServerId]),
?LOG_DEBUG("Server map keys: ~p", [maps:keys(ServerMap)]),
% Convert auth_type from binary to atom if needed
RawAuthType = maps:get(auth_type, ServerMap, open),
AuthType = case RawAuthType of
<<"oauth2">> -> oauth2;
<<"api_key">> -> api_key;
<<"open">> -> open;
Type when is_atom(Type) -> Type;
Type when is_binary(Type) ->
try binary_to_atom(Type) catch _:_ -> open end;
_ -> open % default
end,
?LOG_DEBUG("Converted auth_type ~p -> ~p", [RawAuthType, AuthType]),
% Convert status from binary to atom if needed
RawStatus = maps:get(status, ServerMap, active),
Status = case RawStatus of
<<"active">> -> active;
<<"inactive">> -> inactive;
StatusAtom when is_atom(StatusAtom) -> StatusAtom;
StatusBin when is_binary(StatusBin) ->
try binary_to_atom(StatusBin) catch _:_ -> active end;
_ -> active % default
end,
?LOG_DEBUG("Converted status ~p -> ~p", [RawStatus, Status]),
% Validate required fields
RequiredFields = [id, name, category, url, maintainer],
case validate_required_fields(ServerMap, RequiredFields) of
ok ->
?LOG_DEBUG("All required fields present", []);
{missing, Fields} ->
?LOG_ERROR("Missing required fields: ~p", [Fields]),
{error, {missing_fields, Fields}}
end,
Server = #mcp_server{
id = ServerId,
name = ServerName,
category = maps:get(category, ServerMap),
url = maps:get(url, ServerMap),
auth_type = AuthType,
maintainer = maps:get(maintainer, ServerMap),
description = maps:get(description, ServerMap, <<>>),
capabilities = maps:get(capabilities, ServerMap, []),
status = Status,
last_checked = calendar:local_time(),
metadata = maps:get(metadata, ServerMap, #{})
},
?LOG_DEBUG("Constructed server record for: ~s", [ServerId]),
F = fun() -> mnesia:write(Server) end,
case mnesia:transaction(F) of
{atomic, ok} ->
?LOG_INFO("Successfully added server to database: ~s", [ServerId]),
{ok, Server};
{aborted, {already_exists, _}} ->
?LOG_INFO("Server ~s already exists in database", [ServerId]),
{ok, Server};
{aborted, Reason} ->
?LOG_ERROR("Failed to add server ~s to database: ~p", [ServerId, Reason]),
% Return ok to continue startup even if database fails
case Reason of
{node_not_running, _} ->
?LOG_WARN("Database not available, continuing without persistence", []),
{ok, Server};
_ ->
{error, Reason}
end
end.
update_server(ServerId, Updates) ->
F = fun() ->
case mnesia:read(mcp_server, ServerId) of
[Server] ->
UpdatedServer = update_server_fields(Server, Updates),
mnesia:write(UpdatedServer),
{ok, UpdatedServer};
[] ->
{error, not_found}
end
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end.
delete_server(ServerId) ->
F = fun() -> mnesia:delete({mcp_server, ServerId}) end,
case mnesia:transaction(F) of
{atomic, ok} -> ok;
{aborted, Reason} -> {error, Reason}
end.
get_server(ServerId) ->
F = fun() -> mnesia:read(mcp_server, ServerId) end,
case mnesia:transaction(F) of
{atomic, [Server]} -> {ok, Server};
{atomic, []} -> {error, not_found};
{aborted, Reason} -> {error, Reason}
end.
discover_capabilities(ServerId) ->
case get_server(ServerId) of
{ok, Server} ->
case fetch_server_capabilities(Server#mcp_server.url, Server#mcp_server.auth_type) of
{ok, Capabilities} ->
update_server(ServerId, #{capabilities => Capabilities, last_checked => calendar:local_time()});
Error ->
Error
end;
Error ->
Error
end.
get_server_capabilities(ServerId) ->
case get_server(ServerId) of
{ok, Server} -> {ok, Server#mcp_server.capabilities};
Error -> Error
end.
assign_server_to_agent(AgentId, ServerIds) ->
Mapping = #agent_mcp_mapping{
agent_id = AgentId,
server_ids = ServerIds
},
F = fun() -> mnesia:write(Mapping) end,
case mnesia:transaction(F) of
{atomic, ok} -> ok;
{aborted, Reason} -> {error, Reason}
end.
get_agent_servers(AgentId) ->
F = fun() ->
case mnesia:read(agent_mcp_mapping, AgentId) of
[#agent_mcp_mapping{server_ids = ServerIds}] ->
Servers = lists:foldl(fun(ServerId, Acc) ->
case mnesia:read(mcp_server, ServerId) of
[Server] -> [Server | Acc];
[] -> Acc
end
end, [], ServerIds),
{ok, lists:reverse(Servers)};
[] ->
{ok, []}
end
end,
case mnesia:transaction(F) of
{atomic, Result} -> Result;
{aborted, Reason} -> {error, Reason}
end.
%%%===================================================================
%%% Internal functions
%%%===================================================================
load_seed_servers() ->
% Wait for tables to be available before loading seed servers
mnesia:wait_for_tables([mcp_server], 5000),
lists:foreach(fun(ServerMap) ->
try
case add_server(ServerMap) of
{ok, _Server} ->
io:format("[MCP_CONFIG] Loaded seed server: ~s~n", [maps:get(name, ServerMap, <<"Unknown">>)]);
{error, {aborted, {already_exists, _}}} ->
% Server already exists, that's fine
ok;
{error, AddReason} ->
io:format("[WARNING] Failed to load seed server ~s: ~p~n",
[maps:get(name, ServerMap, <<"Unknown">>), AddReason])
end
catch
Error:Reason:Stack ->
io:format("[ERROR] Exception loading seed server ~s: ~p:~p~n",
[maps:get(name, ServerMap, <<"Unknown">>), Error, Reason]),
io:format("[ERROR] Stack trace: ~p~n", [Stack])
end
end, get_seed_servers()).
update_server_fields(Server, Updates) ->
Server#mcp_server{
name = maps:get(name, Updates, Server#mcp_server.name),
category = maps:get(category, Updates, Server#mcp_server.category),
url = maps:get(url, Updates, Server#mcp_server.url),
auth_type = maps:get(auth_type, Updates, Server#mcp_server.auth_type),
maintainer = maps:get(maintainer, Updates, Server#mcp_server.maintainer),
description = maps:get(description, Updates, Server#mcp_server.description),
capabilities = maps:get(capabilities, Updates, Server#mcp_server.capabilities),
status = maps:get(status, Updates, Server#mcp_server.status),
last_checked = maps:get(last_checked, Updates, Server#mcp_server.last_checked),
metadata = maps:get(metadata, Updates, Server#mcp_server.metadata)
}.
fetch_server_capabilities(Url, AuthType) ->
?LOG_INFO("Fetching capabilities from server: ~s", [Url]),
%% TODO: Implement actual MCP capability discovery
%% This would make an HTTP request to the server's capability endpoint
%% For now, return mock capabilities based on category
?LOG_DEBUG("Using mock capabilities (auth_type: ~p)", [AuthType]),
{ok, [
#{
name => <<"list_tools">>,
description => <<"List available tools">>,
input_schema => #{}
},
#{
name => <<"get_tool_info">>,
description => <<"Get information about a specific tool">>,
input_schema => #{
type => object,
properties => #{
tool_name => #{type => string}
},
required => [tool_name]
}
}
]}.
%% Convert MCP server configuration to Anthropic MCP connector format
to_anthropic_mcp_format(#mcp_server{id = Id, name = Name, url = Url, auth_type = AuthType, metadata = Metadata}) ->
BaseServer = #{
type => <<"url">>,
url => Url,
name => Name
},
% Add authorization token if available
ServerWithAuth = case AuthType of
oauth2 ->
case maps:get(auth_token, Metadata, undefined) of
undefined -> BaseServer;
Token -> maps:put(authorization_token, Token, BaseServer)
end;
api_key ->
case maps:get(api_key, Metadata, undefined) of
undefined -> BaseServer;
ApiKey -> maps:put(authorization_token, ApiKey, BaseServer)
end;
_ -> BaseServer
end,
% Add tool configuration if available
case maps:get(tool_configuration, Metadata, undefined) of
undefined -> ServerWithAuth;
ToolConfig -> maps:put(tool_configuration, ToolConfig, ServerWithAuth)
end.
%% Convert list of server records to Anthropic MCP connector format
to_anthropic_mcp_format_list(Servers) when is_list(Servers) ->
[to_anthropic_mcp_format(Server) || Server <- Servers].
%% Get MCP servers in Anthropic connector format for specific agents
get_anthropic_mcp_servers(AgentIds) when is_list(AgentIds) ->
F = fun() ->
% Get agent-server mappings for specified agents
Mappings = lists:flatten([
qlc:e(qlc:q([Mapping || Mapping <- mnesia:table(agent_mcp_mapping),
Mapping#agent_mcp_mapping.agent_id =:= AgentId]))
|| AgentId <- AgentIds
]),
% Get unique server IDs
ServerIds = lists:usort(lists:flatten([M#agent_mcp_mapping.server_ids || M <- Mappings])),
% Get server records
Servers = lists:flatten([
case mnesia:read(mcp_server, ServerId) of
[Server] -> [Server];
[] -> []
end
|| ServerId <- ServerIds
]),
% Convert to Anthropic format
to_anthropic_mcp_format_list(Servers)
end,
case mnesia:transaction(F) of
{atomic, McpServers} -> {ok, McpServers};
{aborted, Reason} -> {error, Reason}
end;
get_anthropic_mcp_servers(AgentId) when is_binary(AgentId) ->
get_anthropic_mcp_servers([AgentId]).
%% Get all active MCP servers in Anthropic connector format
get_all_anthropic_mcp_servers() ->
case get_all_servers() of
{ok, Servers} ->
ActiveServers = [S || S <- Servers, S#mcp_server.status =:= active],
{ok, to_anthropic_mcp_format_list(ActiveServers)};
Error -> Error
end.
%% Helper function to validate required fields
validate_required_fields(ServerMap, RequiredFields) ->
MissingFields = [Field || Field <- RequiredFields,
not maps:is_key(Field, ServerMap)],
case MissingFields of
[] -> ok;
_ -> {missing, MissingFields}
end.