Skip to content

Commit 9f34de3

Browse files
feat: Add MCP tool annotations for read-only, destructive, and idempotent hints (#839)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 01ffecc commit 9f34de3

File tree

4 files changed

+284
-25
lines changed

4 files changed

+284
-25
lines changed

airbyte/mcp/_annotations.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
2+
"""MCP tool annotation constants.
3+
4+
These constants define the standard MCP annotations for tools, following the
5+
FastMCP 2.2.7+ specification.
6+
7+
For more information, see:
8+
https://gofastmcp.com/concepts/tools#mcp-annotations
9+
"""
10+
11+
from __future__ import annotations
12+
13+
14+
READ_ONLY_HINT = "readOnlyHint"
15+
"""Indicates if the tool only reads data without making any changes.
16+
17+
When True, the tool performs read-only operations and does not modify any state.
18+
When False, the tool may write, create, update, or delete data.
19+
20+
FastMCP default if not specified: False
21+
"""
22+
23+
DESTRUCTIVE_HINT = "destructiveHint"
24+
"""Signals if the tool's changes are destructive (updates or deletes existing data).
25+
26+
This hint is only relevant for non-read-only tools (readOnlyHint=False).
27+
When True, the tool modifies or deletes existing data in a way that may be
28+
difficult or impossible to reverse.
29+
When False, the tool creates new data or performs non-destructive operations.
30+
31+
FastMCP default if not specified: True
32+
"""
33+
34+
IDEMPOTENT_HINT = "idempotentHint"
35+
"""Indicates if repeated calls with the same parameters have the same effect.
36+
37+
When True, calling the tool multiple times with identical parameters produces
38+
the same result and side effects as calling it once.
39+
When False, each call may produce different results or side effects.
40+
41+
FastMCP default if not specified: False
42+
"""
43+
44+
OPEN_WORLD_HINT = "openWorldHint"
45+
"""Specifies if the tool interacts with external systems.
46+
47+
When True, the tool communicates with external services, APIs, or systems
48+
outside the local environment (e.g., cloud APIs, remote databases, internet).
49+
When False, the tool only operates on local state or resources.
50+
51+
FastMCP default if not specified: True
52+
"""

airbyte/mcp/cloud_ops.py

Lines changed: 117 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@
1818
from airbyte.cloud.connectors import CloudDestination, CloudSource, CustomCloudSourceDefinition
1919
from airbyte.cloud.workspaces import CloudWorkspace
2020
from airbyte.destinations.util import get_noop_destination
21+
from airbyte.mcp._annotations import (
22+
DESTRUCTIVE_HINT,
23+
IDEMPOTENT_HINT,
24+
READ_ONLY_HINT,
25+
)
2126
from airbyte.mcp._util import resolve_config, resolve_list_of_strings
2227

2328

@@ -686,18 +691,115 @@ def register_cloud_ops_tools(app: FastMCP) -> None:
686691
687692
This is an internal function and should not be called directly.
688693
"""
689-
app.tool(check_airbyte_cloud_workspace)
690-
app.tool(deploy_source_to_cloud)
691-
app.tool(deploy_destination_to_cloud)
692-
app.tool(deploy_noop_destination_to_cloud)
693-
app.tool(create_connection_on_cloud)
694-
app.tool(run_cloud_sync)
695-
app.tool(get_cloud_sync_status)
696-
app.tool(get_cloud_sync_logs)
697-
app.tool(list_deployed_cloud_source_connectors)
698-
app.tool(list_deployed_cloud_destination_connectors)
699-
app.tool(list_deployed_cloud_connections)
700-
app.tool(publish_custom_source_definition)
701-
app.tool(list_custom_source_definitions)
702-
app.tool(update_custom_source_definition)
703-
app.tool(permanently_delete_custom_source_definition)
694+
app.tool(
695+
check_airbyte_cloud_workspace,
696+
annotations={
697+
READ_ONLY_HINT: True,
698+
IDEMPOTENT_HINT: True,
699+
},
700+
)
701+
702+
app.tool(
703+
deploy_source_to_cloud,
704+
annotations={
705+
DESTRUCTIVE_HINT: False,
706+
},
707+
)
708+
709+
app.tool(
710+
deploy_destination_to_cloud,
711+
annotations={
712+
DESTRUCTIVE_HINT: False,
713+
},
714+
)
715+
716+
app.tool(
717+
deploy_noop_destination_to_cloud,
718+
annotations={
719+
DESTRUCTIVE_HINT: False,
720+
},
721+
)
722+
723+
app.tool(
724+
create_connection_on_cloud,
725+
annotations={
726+
DESTRUCTIVE_HINT: False,
727+
},
728+
)
729+
730+
app.tool(
731+
run_cloud_sync,
732+
annotations={
733+
DESTRUCTIVE_HINT: False,
734+
},
735+
)
736+
737+
app.tool(
738+
get_cloud_sync_status,
739+
annotations={
740+
READ_ONLY_HINT: True,
741+
IDEMPOTENT_HINT: True,
742+
},
743+
)
744+
745+
app.tool(
746+
get_cloud_sync_logs,
747+
annotations={
748+
READ_ONLY_HINT: True,
749+
IDEMPOTENT_HINT: True,
750+
},
751+
)
752+
753+
app.tool(
754+
list_deployed_cloud_source_connectors,
755+
annotations={
756+
READ_ONLY_HINT: True,
757+
IDEMPOTENT_HINT: True,
758+
},
759+
)
760+
761+
app.tool(
762+
list_deployed_cloud_destination_connectors,
763+
annotations={
764+
READ_ONLY_HINT: True,
765+
IDEMPOTENT_HINT: True,
766+
},
767+
)
768+
769+
app.tool(
770+
list_deployed_cloud_connections,
771+
annotations={
772+
READ_ONLY_HINT: True,
773+
IDEMPOTENT_HINT: True,
774+
},
775+
)
776+
777+
app.tool(
778+
publish_custom_source_definition,
779+
annotations={
780+
DESTRUCTIVE_HINT: False,
781+
},
782+
)
783+
784+
app.tool(
785+
list_custom_source_definitions,
786+
annotations={
787+
READ_ONLY_HINT: True,
788+
IDEMPOTENT_HINT: True,
789+
},
790+
)
791+
792+
app.tool(
793+
update_custom_source_definition,
794+
annotations={
795+
DESTRUCTIVE_HINT: True,
796+
},
797+
)
798+
799+
app.tool(
800+
permanently_delete_custom_source_definition,
801+
annotations={
802+
DESTRUCTIVE_HINT: True,
803+
IDEMPOTENT_HINT: True,
804+
},
805+
)

airbyte/mcp/connector_registry.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111

1212
from airbyte._executors.util import DEFAULT_MANIFEST_URL
1313
from airbyte._util.meta import is_docker_installed
14+
from airbyte.mcp._annotations import (
15+
IDEMPOTENT_HINT,
16+
OPEN_WORLD_HINT,
17+
READ_ONLY_HINT,
18+
)
1419
from airbyte.mcp._util import resolve_list_of_strings
1520
from airbyte.sources import get_available_connectors
1621
from airbyte.sources.registry import ConnectorMetadata, get_connector_metadata
@@ -156,5 +161,19 @@ def register_connector_registry_tools(app: FastMCP) -> None:
156161
157162
This is an internal function and should not be called directly.
158163
"""
159-
app.tool(list_connectors)
160-
app.tool(get_connector_info)
164+
app.tool(
165+
list_connectors,
166+
annotations={
167+
READ_ONLY_HINT: True,
168+
IDEMPOTENT_HINT: True,
169+
OPEN_WORLD_HINT: False, # Reads from local registry cache
170+
},
171+
)
172+
173+
app.tool(
174+
get_connector_info,
175+
annotations={
176+
READ_ONLY_HINT: True,
177+
IDEMPOTENT_HINT: True,
178+
},
179+
)

airbyte/mcp/local_ops.py

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
from airbyte import get_source
1414
from airbyte._util.meta import is_docker_installed
1515
from airbyte.caches.util import get_default_cache
16+
from airbyte.mcp._annotations import (
17+
DESTRUCTIVE_HINT,
18+
IDEMPOTENT_HINT,
19+
OPEN_WORLD_HINT,
20+
READ_ONLY_HINT,
21+
)
1622
from airbyte.mcp._util import resolve_config, resolve_list_of_strings
1723
from airbyte.secrets.config import _get_secret_sources
1824
from airbyte.secrets.env_vars import DotenvSecretManager
@@ -767,21 +773,101 @@ def register_local_ops_tools(app: FastMCP) -> None:
767773
768774
This is an internal function and should not be called directly.
769775
"""
770-
app.tool(list_connector_config_secrets)
771-
for tool in (
776+
app.tool(
777+
list_connector_config_secrets,
778+
annotations={
779+
READ_ONLY_HINT: True,
780+
IDEMPOTENT_HINT: True,
781+
},
782+
)
783+
784+
app.tool(
772785
describe_default_cache,
786+
description=(describe_default_cache.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
787+
annotations={
788+
READ_ONLY_HINT: True,
789+
IDEMPOTENT_HINT: True,
790+
OPEN_WORLD_HINT: False, # Local cache only
791+
},
792+
)
793+
794+
app.tool(
773795
get_source_stream_json_schema,
796+
description=(get_source_stream_json_schema.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
797+
annotations={
798+
READ_ONLY_HINT: True,
799+
IDEMPOTENT_HINT: True,
800+
},
801+
)
802+
803+
app.tool(
774804
get_stream_previews,
805+
description=(get_stream_previews.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
806+
annotations={
807+
READ_ONLY_HINT: True,
808+
},
809+
)
810+
811+
app.tool(
775812
list_cached_streams,
813+
description=(list_cached_streams.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
814+
annotations={
815+
READ_ONLY_HINT: True,
816+
IDEMPOTENT_HINT: True,
817+
OPEN_WORLD_HINT: False, # Local cache only
818+
},
819+
)
820+
821+
app.tool(
776822
list_dotenv_secrets,
823+
description=(list_dotenv_secrets.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
824+
annotations={
825+
READ_ONLY_HINT: True,
826+
IDEMPOTENT_HINT: True,
827+
OPEN_WORLD_HINT: False, # Local .env files only
828+
},
829+
)
830+
831+
app.tool(
777832
list_source_streams,
833+
description=(list_source_streams.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
834+
annotations={
835+
READ_ONLY_HINT: True,
836+
IDEMPOTENT_HINT: True,
837+
},
838+
)
839+
840+
app.tool(
778841
read_source_stream_records,
842+
description=(read_source_stream_records.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
843+
annotations={
844+
READ_ONLY_HINT: True,
845+
},
846+
)
847+
848+
app.tool(
779849
run_sql_query,
850+
description=(run_sql_query.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
851+
annotations={
852+
READ_ONLY_HINT: True,
853+
IDEMPOTENT_HINT: True,
854+
OPEN_WORLD_HINT: False, # Local cache only
855+
},
856+
)
857+
858+
app.tool(
780859
sync_source_to_cache,
860+
description=(sync_source_to_cache.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
861+
annotations={
862+
DESTRUCTIVE_HINT: False, # Syncs are additive/merge operations
863+
},
864+
)
865+
866+
app.tool(
781867
validate_connector_config,
782-
):
783-
# Register each tool with the FastMCP app.
784-
app.tool(
785-
tool,
786-
description=(tool.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
787-
)
868+
description=(validate_connector_config.__doc__ or "").rstrip() + "\n" + _CONFIG_HELP,
869+
annotations={
870+
READ_ONLY_HINT: True,
871+
IDEMPOTENT_HINT: True,
872+
},
873+
)

0 commit comments

Comments
 (0)