Skip to content

Commit b1604ba

Browse files
authored
feat: add secret tools (#103)
1 parent ff7cbb6 commit b1604ba

3 files changed

Lines changed: 265 additions & 0 deletions

File tree

src/deepset_mcp/main.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
list_pipeline_templates as list_pipeline_templates_tool,
4444
search_pipeline_templates as search_pipeline_templates_tool,
4545
)
46+
from deepset_mcp.tools.secrets import (
47+
get_secret as get_secret_tool,
48+
list_secrets as list_secrets_tool,
49+
)
4650

4751
INITIALIZED_MODEL = StaticModel.from_pretrained("minishlab/potion-base-2M")
4852

@@ -439,6 +443,34 @@ async def search_pipeline(pipeline_name: str, query: str) -> str:
439443
return response
440444

441445

446+
@mcp.tool()
447+
async def list_secrets(limit: int = 10) -> str:
448+
"""Lists all secrets available in the deepset workspace.
449+
450+
Use this tool to retrieve a list of secrets with their names and IDs.
451+
This is useful for getting an overview of all secrets before retrieving specific ones.
452+
453+
:param limit: Maximum number of secrets to return (default: 10).
454+
"""
455+
async with AsyncDeepsetClient() as client:
456+
response = await list_secrets_tool(client, limit)
457+
return response
458+
459+
460+
@mcp.tool()
461+
async def get_secret(secret_id: str) -> str:
462+
"""Retrieves detailed information about a specific secret by its ID.
463+
464+
Use this tool to get information about a specific secret when you know its ID.
465+
The secret value itself is not returned for security reasons, only metadata.
466+
467+
:param secret_id: The unique identifier of the secret to retrieve.
468+
"""
469+
async with AsyncDeepsetClient() as client:
470+
response = await get_secret_tool(client, secret_id)
471+
return response
472+
473+
442474
# Check if docs search should be enabled
443475
docs_config = get_docs_config()
444476
if docs_config:

src/deepset_mcp/tools/secrets.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from deepset_mcp.api.exceptions import ResourceNotFoundError, UnexpectedAPIError
2+
from deepset_mcp.api.protocols import AsyncClientProtocol
3+
4+
5+
async def list_secrets(client: AsyncClientProtocol, limit: int = 10) -> str:
6+
"""Lists all secrets available in the user's deepset organization.
7+
8+
Use this tool to retrieve a list of secrets with their names and IDs.
9+
This is useful for getting an overview of all secrets before retrieving specific ones.
10+
11+
:param client: The deepset API client
12+
:param limit: Maximum number of secrets to return (default: 10)
13+
14+
:returns: A formatted string containing secret names and IDs
15+
"""
16+
try:
17+
response = await client.secrets().list(limit=limit)
18+
19+
if not response.data:
20+
return "No secrets found in this workspace."
21+
22+
secrets_info = []
23+
for secret in response.data:
24+
secrets_info.append(f"Name: {secret.name}, ID: {secret.secret_id}")
25+
26+
result = f"Found {len(response.data)} secret(s):\n" + "\n".join(secrets_info)
27+
28+
if response.has_more:
29+
result += (
30+
f"\n\nShowing {len(response.data)} of {response.total} total secrets. Use a higher limit to see more."
31+
)
32+
33+
return result
34+
35+
except ResourceNotFoundError as e:
36+
return f"Error: {str(e)}"
37+
except UnexpectedAPIError as e:
38+
return f"API Error: {str(e)}"
39+
except Exception as e:
40+
return f"Unexpected error: {str(e)}"
41+
42+
43+
async def get_secret(client: AsyncClientProtocol, secret_id: str) -> str:
44+
"""Retrieves detailed information about a specific secret by its ID.
45+
46+
Use this tool to get information about a specific secret when you know its ID.
47+
The secret value itself is not returned for security reasons, only metadata.
48+
49+
:param client: The deepset API client
50+
:param secret_id: The unique identifier of the secret to retrieve
51+
52+
:returns: A formatted string containing secret information
53+
"""
54+
try:
55+
response = await client.secrets().get(secret_id)
56+
57+
return f"Secret Details:\nName: {response.name}\nID: {response.secret_id}"
58+
59+
except ResourceNotFoundError as e:
60+
return f"Error: {str(e)}"
61+
except UnexpectedAPIError as e:
62+
return f"API Error: {str(e)}"
63+
except Exception as e:
64+
return f"Unexpected error: {str(e)}"

test/unit/tools/test_secrets.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import pytest
2+
3+
from deepset_mcp.api.exceptions import ResourceNotFoundError, UnexpectedAPIError
4+
from deepset_mcp.api.protocols import SecretResourceProtocol
5+
from deepset_mcp.api.secrets.models import Secret, SecretList
6+
from deepset_mcp.api.shared_models import NoContentResponse
7+
from deepset_mcp.tools.secrets import get_secret, list_secrets
8+
from test.unit.conftest import BaseFakeClient
9+
10+
11+
class FakeSecretResource(SecretResourceProtocol):
12+
def __init__(
13+
self,
14+
list_response: SecretList | None = None,
15+
get_response: Secret | None = None,
16+
list_exception: Exception | None = None,
17+
get_exception: Exception | None = None,
18+
) -> None:
19+
self.list_response = list_response
20+
self.get_response = get_response
21+
self.list_exception = list_exception
22+
self.get_exception = get_exception
23+
24+
async def list(self, limit: int = 10, field: str = "created_at", order: str = "DESC") -> SecretList:
25+
if self.list_exception:
26+
raise self.list_exception
27+
if self.list_response is None:
28+
raise ValueError("No list response configured")
29+
return self.list_response
30+
31+
async def get(self, secret_id: str) -> Secret:
32+
if self.get_exception:
33+
raise self.get_exception
34+
if self.get_response is None:
35+
raise ValueError("No get response configured")
36+
return self.get_response
37+
38+
async def create(self, name: str, secret: str) -> NoContentResponse:
39+
"""Not used in tests, but required by protocol."""
40+
return NoContentResponse(message="Created")
41+
42+
async def delete(self, secret_id: str) -> NoContentResponse:
43+
"""Not used in tests, but required by protocol."""
44+
return NoContentResponse(message="Deleted")
45+
46+
47+
class FakeClientWithSecrets(BaseFakeClient):
48+
def __init__(self, secret_resource: FakeSecretResource) -> None:
49+
super().__init__()
50+
self._secret_resource = secret_resource
51+
52+
def secrets(self) -> FakeSecretResource:
53+
return self._secret_resource
54+
55+
56+
@pytest.mark.asyncio
57+
async def test_list_secrets_success() -> None:
58+
"""Test successful listing of secrets."""
59+
secrets_data = [
60+
Secret(name="api-key", secret_id="secret-1"),
61+
Secret(name="database-password", secret_id="secret-2"),
62+
]
63+
secret_list = SecretList(data=secrets_data, has_more=False, total=2)
64+
fake_resource = FakeSecretResource(list_response=secret_list)
65+
client = FakeClientWithSecrets(fake_resource)
66+
67+
result = await list_secrets(client, limit=10)
68+
69+
expected = "Found 2 secret(s):\nName: api-key, ID: secret-1\nName: database-password, ID: secret-2"
70+
assert result == expected
71+
72+
73+
@pytest.mark.asyncio
74+
async def test_list_secrets_with_pagination() -> None:
75+
"""Test listing secrets with pagination info."""
76+
secrets_data = [
77+
Secret(name="api-key", secret_id="secret-1"),
78+
]
79+
secret_list = SecretList(data=secrets_data, has_more=True, total=5)
80+
fake_resource = FakeSecretResource(list_response=secret_list)
81+
client = FakeClientWithSecrets(fake_resource)
82+
83+
result = await list_secrets(client, limit=1)
84+
85+
expected = (
86+
"Found 1 secret(s):\nName: api-key, ID: secret-1\n\n"
87+
"Showing 1 of 5 total secrets. Use a higher limit to see more."
88+
)
89+
assert result == expected
90+
91+
92+
@pytest.mark.asyncio
93+
async def test_list_secrets_empty() -> None:
94+
"""Test listing when no secrets exist."""
95+
secret_list = SecretList(data=[], has_more=False, total=0)
96+
fake_resource = FakeSecretResource(list_response=secret_list)
97+
client = FakeClientWithSecrets(fake_resource)
98+
99+
result = await list_secrets(client)
100+
101+
assert result == "No secrets found in this workspace."
102+
103+
104+
@pytest.mark.asyncio
105+
async def test_list_secrets_unexpected_api_error() -> None:
106+
"""Test handling of UnexpectedAPIError during list."""
107+
fake_resource = FakeSecretResource(list_exception=UnexpectedAPIError(500, "Internal server error"))
108+
client = FakeClientWithSecrets(fake_resource)
109+
110+
result = await list_secrets(client)
111+
112+
assert result == "API Error: Internal server error (Status Code: 500)"
113+
114+
115+
@pytest.mark.asyncio
116+
async def test_list_secrets_generic_exception() -> None:
117+
"""Test handling of generic exceptions during list."""
118+
fake_resource = FakeSecretResource(list_exception=ValueError("Generic error"))
119+
client = FakeClientWithSecrets(fake_resource)
120+
121+
result = await list_secrets(client)
122+
123+
assert result == "Unexpected error: Generic error"
124+
125+
126+
@pytest.mark.asyncio
127+
async def test_get_secret_success() -> None:
128+
"""Test successful retrieval of a specific secret."""
129+
secret = Secret(name="api-key", secret_id="secret-1")
130+
fake_resource = FakeSecretResource(get_response=secret)
131+
client = FakeClientWithSecrets(fake_resource)
132+
133+
result = await get_secret(client, "secret-1")
134+
135+
expected = "Secret Details:\nName: api-key\nID: secret-1"
136+
assert result == expected
137+
138+
139+
@pytest.mark.asyncio
140+
async def test_get_secret_not_found() -> None:
141+
"""Test handling when secret is not found."""
142+
fake_resource = FakeSecretResource(get_exception=ResourceNotFoundError("Secret 'nonexistent' not found."))
143+
client = FakeClientWithSecrets(fake_resource)
144+
145+
result = await get_secret(client, "nonexistent")
146+
147+
assert result == "Error: Secret 'nonexistent' not found. (Status Code: 404)"
148+
149+
150+
@pytest.mark.asyncio
151+
async def test_get_secret_unexpected_api_error() -> None:
152+
"""Test handling of UnexpectedAPIError during get."""
153+
fake_resource = FakeSecretResource(get_exception=UnexpectedAPIError(500, "Server error"))
154+
client = FakeClientWithSecrets(fake_resource)
155+
156+
result = await get_secret(client, "secret-1")
157+
158+
assert result == "API Error: Server error (Status Code: 500)"
159+
160+
161+
@pytest.mark.asyncio
162+
async def test_get_secret_generic_exception() -> None:
163+
"""Test handling of generic exceptions during get."""
164+
fake_resource = FakeSecretResource(get_exception=ValueError("Something went wrong"))
165+
client = FakeClientWithSecrets(fake_resource)
166+
167+
result = await get_secret(client, "secret-1")
168+
169+
assert result == "Unexpected error: Something went wrong"

0 commit comments

Comments
 (0)