From 3b1b37862e2cd019c34ab85186612dbb5fea1887 Mon Sep 17 00:00:00 2001 From: Jaideep Rao Date: Mon, 1 Dec 2025 10:43:12 +0530 Subject: [PATCH] feat: add connectors implementation for static config Signed-off-by: Jaideep Rao --- client-sdks/stainless/openapi.yml | 90 +++++---- docs/static/deprecated-llama-stack-spec.yaml | 71 ++++--- .../static/experimental-llama-stack-spec.yaml | 90 +++++---- docs/static/llama-stack-spec.yaml | 71 ++++--- docs/static/stainless-llama-stack-spec.yaml | 90 +++++---- src/llama_stack/core/connectors/__init__.py | 5 + src/llama_stack/core/connectors/connectors.py | 175 ++++++++++++++++++ src/llama_stack/core/datatypes.py | 2 + src/llama_stack/core/server/server.py | 1 + src/llama_stack/core/stack.py | 9 + src/llama_stack/log.py | 1 + src/llama_stack/providers/utils/tools/mcp.py | 41 ++++ src/llama_stack_api/common/errors.py | 22 +++ src/llama_stack_api/connectors.py | 93 ++++++---- 14 files changed, 558 insertions(+), 203 deletions(-) create mode 100644 src/llama_stack/core/connectors/__init__.py create mode 100644 src/llama_stack/core/connectors/connectors.py diff --git a/client-sdks/stainless/openapi.yml b/client-sdks/stainless/openapi.yml index ee63c7741..a2b6880ab 100644 --- a/client-sdks/stainless/openapi.yml +++ b/client-sdks/stainless/openapi.yml @@ -3903,23 +3903,30 @@ paths: schema: $ref: '#/components/schemas/Connector' '400': - description: Bad Request $ref: '#/components/responses/BadRequest400' + description: Bad Request '429': - description: Too Many Requests $ref: '#/components/responses/TooManyRequests429' + description: Too Many Requests '500': - description: Internal Server Error $ref: '#/components/responses/InternalServerError500' + description: Internal Server Error default: - description: Default Response $ref: '#/components/responses/DefaultError' + description: Default Response tags: - Connectors summary: Get Connector description: Get a connector by its ID. operationId: get_connector_v1alpha_connectors__connector_id__get parameters: + - name: include_tools + in: query + required: false + schema: + type: boolean + default: false + title: Include Tools - name: connector_id in: path required: true @@ -3930,11 +3937,11 @@ paths: get: responses: '200': - description: A MCPListToolsTool. + description: A ToolDef. content: application/json: schema: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' '400': description: Bad Request $ref: '#/components/responses/BadRequest400' @@ -11884,7 +11891,7 @@ components: tools: anyOf: - items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array - type: 'null' description: List of tools available from the connector @@ -12026,7 +12033,7 @@ components: properties: data: items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array title: Data type: object @@ -12975,6 +12982,33 @@ components: - url title: RegistryInput type: object + ToolGroupInput: + description: Input data for registering a tool group. + properties: + toolgroup_id: + title: Toolgroup Id + type: string + provider_id: + title: Provider Id + type: string + args: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + nullable: true + mcp_endpoint: + anyOf: + - $ref: '#/components/schemas/URL' + title: URL + - type: 'null' + nullable: true + title: URL + required: + - toolgroup_id + - provider_id + title: ToolGroupInput + type: object ConnectorInput: description: Input for creating a connector. properties: @@ -12991,6 +13025,19 @@ components: description: URL of the connector title: Url type: string + headers: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + description: HTTP headers to include when connecting + nullable: true + authorization: + anyOf: + - type: string + - type: 'null' + description: OAuth access token for authentication + nullable: true required: - url title: ConnectorInput @@ -13079,33 +13126,6 @@ components: - items title: ConversationItemCreateRequest type: object - ToolGroupInput: - description: Input data for registering a tool group. - properties: - toolgroup_id: - title: Toolgroup Id - type: string - provider_id: - title: Provider Id - type: string - args: - anyOf: - - additionalProperties: true - type: object - - type: 'null' - nullable: true - mcp_endpoint: - anyOf: - - $ref: '#/components/schemas/URL' - title: URL - - type: 'null' - nullable: true - title: URL - required: - - toolgroup_id - - provider_id - title: ToolGroupInput - type: object Api: description: Enumeration of all available APIs in the Llama Stack system. enum: diff --git a/docs/static/deprecated-llama-stack-spec.yaml b/docs/static/deprecated-llama-stack-spec.yaml index 972ad317c..559b27133 100644 --- a/docs/static/deprecated-llama-stack-spec.yaml +++ b/docs/static/deprecated-llama-stack-spec.yaml @@ -8672,7 +8672,7 @@ components: tools: anyOf: - items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array - type: 'null' description: List of tools available from the connector @@ -8814,7 +8814,7 @@ components: properties: data: items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array title: Data type: object @@ -9763,6 +9763,33 @@ components: - url title: RegistryInput type: object + ToolGroupInput: + description: Input data for registering a tool group. + properties: + toolgroup_id: + title: Toolgroup Id + type: string + provider_id: + title: Provider Id + type: string + args: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + nullable: true + mcp_endpoint: + anyOf: + - $ref: '#/components/schemas/URL' + title: URL + - type: 'null' + nullable: true + title: URL + required: + - toolgroup_id + - provider_id + title: ToolGroupInput + type: object ConnectorInput: description: Input for creating a connector. properties: @@ -9779,6 +9806,19 @@ components: description: URL of the connector title: Url type: string + headers: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + description: HTTP headers to include when connecting + nullable: true + authorization: + anyOf: + - type: string + - type: 'null' + description: OAuth access token for authentication + nullable: true required: - url title: ConnectorInput @@ -9867,33 +9907,6 @@ components: - items title: ConversationItemCreateRequest type: object - ToolGroupInput: - description: Input data for registering a tool group. - properties: - toolgroup_id: - title: Toolgroup Id - type: string - provider_id: - title: Provider Id - type: string - args: - anyOf: - - additionalProperties: true - type: object - - type: 'null' - nullable: true - mcp_endpoint: - anyOf: - - $ref: '#/components/schemas/URL' - title: URL - - type: 'null' - nullable: true - title: URL - required: - - toolgroup_id - - provider_id - title: ToolGroupInput - type: object Api: description: Enumeration of all available APIs in the Llama Stack system. enum: diff --git a/docs/static/experimental-llama-stack-spec.yaml b/docs/static/experimental-llama-stack-spec.yaml index 27cb82b94..05746ca52 100644 --- a/docs/static/experimental-llama-stack-spec.yaml +++ b/docs/static/experimental-llama-stack-spec.yaml @@ -640,23 +640,30 @@ paths: schema: $ref: '#/components/schemas/Connector' '400': - description: Bad Request $ref: '#/components/responses/BadRequest400' + description: Bad Request '429': - description: Too Many Requests $ref: '#/components/responses/TooManyRequests429' + description: Too Many Requests '500': - description: Internal Server Error $ref: '#/components/responses/InternalServerError500' + description: Internal Server Error default: - description: Default Response $ref: '#/components/responses/DefaultError' + description: Default Response tags: - Connectors summary: Get Connector description: Get a connector by its ID. operationId: get_connector_v1alpha_connectors__connector_id__get parameters: + - name: include_tools + in: query + required: false + schema: + type: boolean + default: false + title: Include Tools - name: connector_id in: path required: true @@ -667,11 +674,11 @@ paths: get: responses: '200': - description: A MCPListToolsTool. + description: A ToolDef. content: application/json: schema: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' '400': description: Bad Request $ref: '#/components/responses/BadRequest400' @@ -7725,7 +7732,7 @@ components: tools: anyOf: - items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array - type: 'null' description: List of tools available from the connector @@ -7855,7 +7862,7 @@ components: properties: data: items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array title: Data type: object @@ -8735,6 +8742,33 @@ components: - url title: RegistryInput type: object + ToolGroupInput: + description: Input data for registering a tool group. + properties: + toolgroup_id: + title: Toolgroup Id + type: string + provider_id: + title: Provider Id + type: string + args: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + nullable: true + mcp_endpoint: + anyOf: + - $ref: '#/components/schemas/URL' + title: URL + - type: 'null' + nullable: true + title: URL + required: + - toolgroup_id + - provider_id + title: ToolGroupInput + type: object ConnectorInput: description: Input for creating a connector. properties: @@ -8751,6 +8785,19 @@ components: description: URL of the connector title: Url type: string + headers: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + description: HTTP headers to include when connecting + nullable: true + authorization: + anyOf: + - type: string + - type: 'null' + description: OAuth access token for authentication + nullable: true required: - url title: ConnectorInput @@ -8839,33 +8886,6 @@ components: - items title: ConversationItemCreateRequest type: object - ToolGroupInput: - description: Input data for registering a tool group. - properties: - toolgroup_id: - title: Toolgroup Id - type: string - provider_id: - title: Provider Id - type: string - args: - anyOf: - - additionalProperties: true - type: object - - type: 'null' - nullable: true - mcp_endpoint: - anyOf: - - $ref: '#/components/schemas/URL' - title: URL - - type: 'null' - nullable: true - title: URL - required: - - toolgroup_id - - provider_id - title: ToolGroupInput - type: object Api: description: Enumeration of all available APIs in the Llama Stack system. enum: diff --git a/docs/static/llama-stack-spec.yaml b/docs/static/llama-stack-spec.yaml index 01e8c1ae1..30dc10bdc 100644 --- a/docs/static/llama-stack-spec.yaml +++ b/docs/static/llama-stack-spec.yaml @@ -10114,7 +10114,7 @@ components: tools: anyOf: - items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array - type: 'null' description: List of tools available from the connector @@ -10256,7 +10256,7 @@ components: properties: data: items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array title: Data type: object @@ -11205,6 +11205,33 @@ components: - url title: RegistryInput type: object + ToolGroupInput: + description: Input data for registering a tool group. + properties: + toolgroup_id: + title: Toolgroup Id + type: string + provider_id: + title: Provider Id + type: string + args: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + nullable: true + mcp_endpoint: + anyOf: + - $ref: '#/components/schemas/URL' + title: URL + - type: 'null' + nullable: true + title: URL + required: + - toolgroup_id + - provider_id + title: ToolGroupInput + type: object ConnectorInput: description: Input for creating a connector. properties: @@ -11221,6 +11248,19 @@ components: description: URL of the connector title: Url type: string + headers: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + description: HTTP headers to include when connecting + nullable: true + authorization: + anyOf: + - type: string + - type: 'null' + description: OAuth access token for authentication + nullable: true required: - url title: ConnectorInput @@ -11309,33 +11349,6 @@ components: - items title: ConversationItemCreateRequest type: object - ToolGroupInput: - description: Input data for registering a tool group. - properties: - toolgroup_id: - title: Toolgroup Id - type: string - provider_id: - title: Provider Id - type: string - args: - anyOf: - - additionalProperties: true - type: object - - type: 'null' - nullable: true - mcp_endpoint: - anyOf: - - $ref: '#/components/schemas/URL' - title: URL - - type: 'null' - nullable: true - title: URL - required: - - toolgroup_id - - provider_id - title: ToolGroupInput - type: object Api: description: Enumeration of all available APIs in the Llama Stack system. enum: diff --git a/docs/static/stainless-llama-stack-spec.yaml b/docs/static/stainless-llama-stack-spec.yaml index ee63c7741..a2b6880ab 100644 --- a/docs/static/stainless-llama-stack-spec.yaml +++ b/docs/static/stainless-llama-stack-spec.yaml @@ -3903,23 +3903,30 @@ paths: schema: $ref: '#/components/schemas/Connector' '400': - description: Bad Request $ref: '#/components/responses/BadRequest400' + description: Bad Request '429': - description: Too Many Requests $ref: '#/components/responses/TooManyRequests429' + description: Too Many Requests '500': - description: Internal Server Error $ref: '#/components/responses/InternalServerError500' + description: Internal Server Error default: - description: Default Response $ref: '#/components/responses/DefaultError' + description: Default Response tags: - Connectors summary: Get Connector description: Get a connector by its ID. operationId: get_connector_v1alpha_connectors__connector_id__get parameters: + - name: include_tools + in: query + required: false + schema: + type: boolean + default: false + title: Include Tools - name: connector_id in: path required: true @@ -3930,11 +3937,11 @@ paths: get: responses: '200': - description: A MCPListToolsTool. + description: A ToolDef. content: application/json: schema: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' '400': description: Bad Request $ref: '#/components/responses/BadRequest400' @@ -11884,7 +11891,7 @@ components: tools: anyOf: - items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array - type: 'null' description: List of tools available from the connector @@ -12026,7 +12033,7 @@ components: properties: data: items: - $ref: '#/components/schemas/MCPListToolsTool' + $ref: '#/components/schemas/ToolDef' type: array title: Data type: object @@ -12975,6 +12982,33 @@ components: - url title: RegistryInput type: object + ToolGroupInput: + description: Input data for registering a tool group. + properties: + toolgroup_id: + title: Toolgroup Id + type: string + provider_id: + title: Provider Id + type: string + args: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + nullable: true + mcp_endpoint: + anyOf: + - $ref: '#/components/schemas/URL' + title: URL + - type: 'null' + nullable: true + title: URL + required: + - toolgroup_id + - provider_id + title: ToolGroupInput + type: object ConnectorInput: description: Input for creating a connector. properties: @@ -12991,6 +13025,19 @@ components: description: URL of the connector title: Url type: string + headers: + anyOf: + - additionalProperties: true + type: object + - type: 'null' + description: HTTP headers to include when connecting + nullable: true + authorization: + anyOf: + - type: string + - type: 'null' + description: OAuth access token for authentication + nullable: true required: - url title: ConnectorInput @@ -13079,33 +13126,6 @@ components: - items title: ConversationItemCreateRequest type: object - ToolGroupInput: - description: Input data for registering a tool group. - properties: - toolgroup_id: - title: Toolgroup Id - type: string - provider_id: - title: Provider Id - type: string - args: - anyOf: - - additionalProperties: true - type: object - - type: 'null' - nullable: true - mcp_endpoint: - anyOf: - - $ref: '#/components/schemas/URL' - title: URL - - type: 'null' - nullable: true - title: URL - required: - - toolgroup_id - - provider_id - title: ToolGroupInput - type: object Api: description: Enumeration of all available APIs in the Llama Stack system. enum: diff --git a/src/llama_stack/core/connectors/__init__.py b/src/llama_stack/core/connectors/__init__.py new file mode 100644 index 000000000..756f351d8 --- /dev/null +++ b/src/llama_stack/core/connectors/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. diff --git a/src/llama_stack/core/connectors/connectors.py b/src/llama_stack/core/connectors/connectors.py new file mode 100644 index 000000000..027b6fd1b --- /dev/null +++ b/src/llama_stack/core/connectors/connectors.py @@ -0,0 +1,175 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +from datetime import UTC, datetime +from typing import Any + +from pydantic import BaseModel + +from llama_stack.core.datatypes import StackRunConfig +from llama_stack.log import get_logger +from llama_stack.providers.utils.tools.mcp import get_mcp_server_info, list_mcp_tools +from llama_stack_api import ( + Connector, + Connectors, + ConnectorType, + ListConnectorsResponse, + ListRegistriesResponse, + ListToolsResponse, + Registry, + ToolDef, +) +from llama_stack_api.common.errors import ( + ConnectorNotFoundError, + ConnectorToolNotFoundError, + RegistryNotFoundError, +) + +logger = get_logger(name=__name__, category="connectors") + + +class ConnectorServiceConfig(BaseModel): + """Configuration for the built-in connector service. + + :param run_config: Stack run configuration for resolving persistence + """ + + run_config: StackRunConfig + + +async def get_provider_impl(config: ConnectorServiceConfig): + """Get the connector service implementation.""" + impl = ConnectorServiceImpl(config) + return impl + + +class ConnectorServiceImpl(Connectors): + """Built-in connector service implementation.""" + + def __init__(self, config: ConnectorServiceConfig): + self.config = config + # TODO: should these be stored in a kvstore? + self.connectors_map: dict[str, Connector] = {} + self.registries_map: dict[str, Registry] = {} + + async def register_connector( + self, + url: str, + connector_id: str | None = None, + connector_type: ConnectorType = ConnectorType.MCP, + headers: dict[str, Any] | None = None, + authorization: str | None = None, + ) -> Connector: + """Register a new connector. + + :param url: URL of the MCP server to connect to. + :param connector_id: (Optional) User-specified identifier for the connector. + :param connector_type: (Optional) Type of connector, defaults to MCP. + :param headers: (Optional) HTTP headers to include when connecting to the server. + :param authorization: (Optional) OAuth access token for authenticating with the MCP server. + :returns: The registered Connector. + """ + # Fetch server info and tools from the MCP server + # TODO: documentation item: users should be able to pass headers and authorization in the connector input as env variables. + server_info = await get_mcp_server_info(url, headers=headers, authorization=authorization) + tools_response = await list_mcp_tools(url, headers=headers, authorization=authorization) + + connector = Connector( + identifier=server_info.name, + provider_id="builtin::connectors", + user_connector_id=connector_id, + connector_type=connector_type, + url=url, + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + server_name=server_info.name, + server_label=server_info.title, + server_description=server_info.description, + tools=tools_response.data, + ) + + logger.info(f"Registered connector {connector.connector_id} with server name {connector.server_name}") + self.connectors_map[connector.connector_id] = connector + return connector + + async def list_connectors( + self, + registry_id: str | None = None, + include_tools: bool = False, + ) -> ListConnectorsResponse: + """List all configured connectors. + + :param registry_id: (Optional) The ID of a registry to filter connectors for. + :param include_tools: (Optional) Whether to include tools in the response. + :returns: A ListConnectorsResponse. + """ + connectors = [c for c in self.connectors_map.values() if registry_id is None or c.registry_id == registry_id] + if not include_tools: + return ListConnectorsResponse(data=[c.without_tools for c in connectors]) + return ListConnectorsResponse(data=connectors) + + async def get_connector(self, connector_id: str, include_tools: bool = False) -> Connector: + """Get a connector by its ID. + + :param connector_id: The ID of the connector to get. + :returns: A Connector. + :raises ConnectorNotFoundError: If the connector is not found. + """ + connector = self.connectors_map.get(connector_id) + if connector is None: + raise ConnectorNotFoundError(connector_id) + if not include_tools: + return connector.without_tools + return connector + + async def list_connector_tools(self, connector_id: str) -> ListToolsResponse: + """List tools available from a connector. + + :param connector_id: The ID of the connector to list tools for. + :returns: A ListToolsResponse. + :raises ConnectorNotFoundError: If the connector is not found. + """ + connector = await self.get_connector(connector_id, include_tools=True) + # Return empty list if no tools, rather than raising + return ListToolsResponse(data=connector.tools or []) + + async def get_connector_tool(self, connector_id: str, tool_name: str) -> ToolDef: + """Get a tool definition by its name from a connector. + + :param connector_id: The ID of the connector to get the tool from. + :param tool_name: The name of the tool to get. + :returns: A ToolDef. + :raises ConnectorNotFoundError: If the connector is not found. + :raises ConnectorToolNotFoundError: If the tool is not found in the connector. + """ + connector_tools = await self.list_connector_tools(connector_id) + for tool in connector_tools.data: + if tool.name == tool_name: + return tool + raise ConnectorToolNotFoundError(connector_id, tool_name) + + async def list_registries(self) -> ListRegistriesResponse: + """List all registries. + + :returns: A ListRegistriesResponse. + """ + return ListRegistriesResponse(data=list(self.registries_map.values())) + + async def get_registry(self, registry_id: str) -> Registry: + """Get a registry by its ID. + + :param registry_id: The ID of the registry to get. + :returns: A Registry. + :raises RegistryNotFoundError: If the registry is not found. + """ + registry = self.registries_map.get(registry_id) + if registry is None: + raise RegistryNotFoundError(registry_id) + return registry + + async def shutdown(self) -> None: + self.connectors_map.clear() + self.registries_map.clear() diff --git a/src/llama_stack/core/datatypes.py b/src/llama_stack/core/datatypes.py index f64286ef5..2fc2f35b9 100644 --- a/src/llama_stack/core/datatypes.py +++ b/src/llama_stack/core/datatypes.py @@ -22,6 +22,7 @@ from llama_stack_api import ( Api, Benchmark, BenchmarkInput, + ConnectorInput, Dataset, DatasetInput, DatasetIO, @@ -429,6 +430,7 @@ class RegisteredResources(BaseModel): scoring_fns: list[ScoringFnInput] = Field(default_factory=list) benchmarks: list[BenchmarkInput] = Field(default_factory=list) tool_groups: list[ToolGroupInput] = Field(default_factory=list) + connectors: list[ConnectorInput] = Field(default_factory=list) class ServerConfig(BaseModel): diff --git a/src/llama_stack/core/server/server.py b/src/llama_stack/core/server/server.py index 9a01eb75e..f5f9ce78f 100644 --- a/src/llama_stack/core/server/server.py +++ b/src/llama_stack/core/server/server.py @@ -457,6 +457,7 @@ def create_app() -> StackApp: apis_to_serve.add("providers") apis_to_serve.add("prompts") apis_to_serve.add("conversations") + apis_to_serve.add("connectors") for api_str in apis_to_serve: api = Api(api_str) diff --git a/src/llama_stack/core/stack.py b/src/llama_stack/core/stack.py index 8ba1f2afd..7a6b9c253 100644 --- a/src/llama_stack/core/stack.py +++ b/src/llama_stack/core/stack.py @@ -13,6 +13,7 @@ from typing import Any import yaml +from llama_stack.core.connectors.connectors import ConnectorServiceConfig, ConnectorServiceImpl from llama_stack.core.conversations.conversations import ConversationServiceConfig, ConversationServiceImpl from llama_stack.core.datatypes import Provider, SafetyConfig, StackRunConfig, VectorStoresConfig from llama_stack.core.distribution import get_provider_registry @@ -39,6 +40,7 @@ from llama_stack_api import ( Api, Batches, Benchmarks, + Connectors, Conversations, DatasetIO, Datasets, @@ -64,6 +66,7 @@ logger = get_logger(name=__name__, category="core") class LlamaStack( Providers, + Connectors, Inference, Agents, Batches, @@ -100,6 +103,7 @@ RESOURCES = [ ), ("benchmarks", Api.benchmarks, "register_benchmark", "list_benchmarks"), ("tool_groups", Api.tool_groups, "register_tool_group", "list_tool_groups"), + ("connectors", Api.connectors, "register_connector", "list_connectors"), ] @@ -372,6 +376,11 @@ def add_internal_implementations(impls: dict[Api, Any], run_config: StackRunConf ) impls[Api.conversations] = conversations_impl + connectors_impl = ConnectorServiceImpl( + ConnectorServiceConfig(run_config=run_config), + ) + impls[Api.connectors] = connectors_impl + def _initialize_storage(run_config: StackRunConfig): kv_backends: dict[str, StorageBackendConfig] = {} diff --git a/src/llama_stack/log.py b/src/llama_stack/log.py index a44a0ac26..d753be660 100644 --- a/src/llama_stack/log.py +++ b/src/llama_stack/log.py @@ -50,6 +50,7 @@ CATEGORIES = [ "post_training", "scoring", "tests", + "connectors", ] UNCATEGORIZED = "uncategorized" diff --git a/src/llama_stack/providers/utils/tools/mcp.py b/src/llama_stack/providers/utils/tools/mcp.py index 05cdfa73b..165ceb419 100644 --- a/src/llama_stack/providers/utils/tools/mcp.py +++ b/src/llama_stack/providers/utils/tools/mcp.py @@ -228,3 +228,44 @@ async def invoke_mcp_tool( content=content, error_code=1 if result.isError else 0, ) + + +from dataclasses import dataclass + + +@dataclass +class MCPServerInfo: + """Server information from an MCP server.""" + + name: str + version: str + title: str | None = None + description: str | None = None + + +async def get_mcp_server_info( + endpoint: str, + headers: dict[str, str] | None = None, + authorization: str | None = None, +) -> MCPServerInfo: + """Get server info from an MCP server. + + Args: + endpoint: MCP server endpoint URL + headers: Optional base headers to include + authorization: Optional OAuth access token (just the token, not "Bearer ") + + Returns: + MCPServerInfo containing name, version, title, and description + """ + final_headers = prepare_mcp_headers(headers, authorization) + + async with client_wrapper(endpoint, final_headers) as session: + init_result = await session.initialize() + + return MCPServerInfo( + name=init_result.serverInfo.name, + version=init_result.serverInfo.version, + title=init_result.serverInfo.title, + description=init_result.instructions, + ) diff --git a/src/llama_stack_api/common/errors.py b/src/llama_stack_api/common/errors.py index de938b249..e197f19e3 100644 --- a/src/llama_stack_api/common/errors.py +++ b/src/llama_stack_api/common/errors.py @@ -80,6 +80,28 @@ class TokenValidationError(ValueError): super().__init__(message) +class ConnectorNotFoundError(ResourceNotFoundError): + """raised when Llama Stack cannot find a referenced connector""" + + def __init__(self, connector_id: str) -> None: + super().__init__(connector_id, "Connector", "client.connectors.list()") + + +class ConnectorToolNotFoundError(ValueError): + """raised when Llama Stack cannot find a referenced tool in a connector""" + + def __init__(self, connector_id: str, tool_name: str) -> None: + message = f"Tool '{tool_name}' not found in connector '{connector_id}'. Use 'client.connectors.list_tools(\"{connector_id}\")' to list available tools." + super().__init__(message) + + +class RegistryNotFoundError(ResourceNotFoundError): + """raised when Llama Stack cannot find a referenced registry""" + + def __init__(self, registry_id: str) -> None: + super().__init__(registry_id, "Registry", "client.connectors.list_registries()") + + class ConversationNotFoundError(ResourceNotFoundError): """raised when Llama Stack cannot find a referenced conversation""" diff --git a/src/llama_stack_api/connectors.py b/src/llama_stack_api/connectors.py index e7adcc8f1..cedfa365e 100644 --- a/src/llama_stack_api/connectors.py +++ b/src/llama_stack_api/connectors.py @@ -6,15 +6,15 @@ from datetime import datetime from enum import StrEnum -from typing import Literal, Protocol +from typing import Any, Literal, Protocol from pydantic import BaseModel, Field from typing_extensions import runtime_checkable -from llama_stack_api.openai_responses import MCPListToolsTool from llama_stack_api.registries import ListRegistriesResponse, Registry from llama_stack_api.resource import Resource, ResourceType from llama_stack_api.schema_utils import json_schema_type, webmethod +from llama_stack_api.tools import ToolDef from llama_stack_api.version import LLAMA_STACK_API_V1ALPHA @@ -54,7 +54,9 @@ class Connector(Resource): server_name: str | None = Field(default=None, description="Name of the server") server_label: str | None = Field(default=None, description="Label of the server") server_description: str | None = Field(default=None, description="Description of the server") - tools: list[MCPListToolsTool] | None = Field(default=None, description="List of tools available from the connector") + # TODO: using ToolDef for now, but MCPListToolsTool should probably be updated and used instead + # once toolgroups are removed completely + tools: list[ToolDef] | None = Field(default=None, description="List of tools available from the connector") registry_id: str | None = Field(default=None, description="ID of the registry this connector belongs to") def _generate_connector_id(self) -> str: @@ -67,6 +69,11 @@ class Connector(Resource): def connector_id(self) -> str: return self.user_connector_id if self.user_connector_id is not None else self._generate_connector_id() + @property + def without_tools(self) -> "Connector": + """Return a copy of this connector with tools removed.""" + return self.model_copy(update={"tools": None}) + @json_schema_type class ConnectorInput(BaseModel): @@ -75,11 +82,15 @@ class ConnectorInput(BaseModel): :param connector_type: Type of connector :param connector_id: Unique identifier for the connector :param url: URL of the connector + :param headers: (Optional) HTTP headers to include when connecting to the server + :param authorization: (Optional) OAuth access token for authenticating with the MCP server """ connector_type: ConnectorType = Field(default=ConnectorType.MCP) connector_id: str | None = Field(default=None, description="Unique identifier for the connector") url: str = Field(..., description="URL of the connector") + headers: dict[str, Any] | None = Field(default=None, description="HTTP headers to include when connecting") + authorization: str | None = Field(default=None, description="OAuth access token for authentication") @json_schema_type @@ -99,11 +110,14 @@ class ListToolsResponse(BaseModel): :param data: List of tools """ - data: list[MCPListToolsTool] + data: list[ToolDef] @runtime_checkable class Connectors(Protocol): + # NOTE: Route order matters! More specific routes must come before less specific ones. + # Routes with {param:path} are greedy and will match everything including slashes. + @webmethod(route="/connectors", method="GET", level=LLAMA_STACK_API_V1ALPHA) async def list_connectors( self, @@ -118,19 +132,38 @@ class Connectors(Protocol): """ ... - @webmethod(route="/connectors/{connector_id:path}", method="GET", level=LLAMA_STACK_API_V1ALPHA) - async def get_connector( - self, - connector_id: str, - ) -> Connector: - """Get a connector by its ID. + @webmethod(route="/connectors/registries", method="GET", level=LLAMA_STACK_API_V1ALPHA) + async def list_registries(self) -> ListRegistriesResponse: + """List all registries. - :param connector_id: The ID of the connector to get. - :returns: A Connector. + :returns: A ListRegistriesResponse. """ ... - @webmethod(route="/connectors/{connector_id:path}/tools", method="GET", level=LLAMA_STACK_API_V1ALPHA) + @webmethod(route="/connectors/registries/{registry_id}", method="GET", level=LLAMA_STACK_API_V1ALPHA) + async def get_registry(self, registry_id: str) -> Registry: + """Get a registry by its ID. + + :param registry_id: The ID of the registry to get. + :returns: A Registry. + """ + ... + + @webmethod(route="/connectors/{connector_id}/tools/{tool_name}", method="GET", level=LLAMA_STACK_API_V1ALPHA) + async def get_connector_tool( + self, + connector_id: str, + tool_name: str, + ) -> ToolDef: + """Get a tool definition by its name from a connector. + + :param connector_id: The ID of the connector to get the tool from. + :param tool_name: The name of the tool to get. + :returns: A ToolDef. + """ + ... + + @webmethod(route="/connectors/{connector_id}/tools", method="GET", level=LLAMA_STACK_API_V1ALPHA) async def list_connector_tools( self, connector_id: str, @@ -142,35 +175,15 @@ class Connectors(Protocol): """ ... - @webmethod( - route="/connectors/{connector_id:path}/tools/{tool_name:path}", method="GET", level=LLAMA_STACK_API_V1ALPHA - ) - async def get_connector_tool( + @webmethod(route="/connectors/{connector_id}", method="GET", level=LLAMA_STACK_API_V1ALPHA) + async def get_connector( self, connector_id: str, - tool_name: str, - ) -> MCPListToolsTool: - """Get a tool definition by its name from a connector. + include_tools: bool = False, + ) -> Connector: + """Get a connector by its ID. - :param connector_id: The ID of the connector to get the tool from. - :param tool_name: The name of the tool to get. - :returns: A MCPListToolsTool. - """ - ... - - @webmethod(route="/connectors/registries", method="GET", level=LLAMA_STACK_API_V1ALPHA) - async def list_registries(self) -> ListRegistriesResponse: - """List all registries. - - :returns: A ListRegistriesResponse. - """ - ... - - @webmethod(route="/connectors/registries/{registry_id:path}", method="GET", level=LLAMA_STACK_API_V1ALPHA) - async def get_registry(self, registry_id: str) -> Registry: - """Get a registry by its ID. - - :param registry_id: The ID of the registry to get. - :returns: A Registry. + :param connector_id: The ID of the connector to get. + :returns: A Connector. """ ...