agentic-fabriq-sdk 0.1.23__tar.gz → 0.1.25__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agentic-fabriq-sdk might be problematic. Click here for more details.
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/PKG-INFO +22 -3
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/README.md +21 -2
- agentic_fabriq_sdk-0.1.25/af_cli/__init__.py +17 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/applications.py +121 -12
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/auth.py +8 -7
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/config.py +37 -11
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/tools.py +147 -15
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/client.py +6 -1
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/config.py +2 -1
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/main.py +6 -10
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/oauth.py +3 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/connectors/__init__.py +0 -2
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/connectors/base.py +9 -42
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/dx/__init__.py +1 -2
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/dx/runtime.py +0 -22
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/fabriq_client.py +0 -38
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/models/__init__.py +0 -4
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/models/types.py +0 -22
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/pyproject.toml +1 -1
- agentic_fabriq_sdk-0.1.23/af_cli/__init__.py +0 -8
- agentic_fabriq_sdk-0.1.23/af_cli/commands/mcp_servers.py +0 -83
- agentic_fabriq_sdk-0.1.23/af_cli/commands/secrets.py +0 -109
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/oauth.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/output.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/token_storage.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/applications.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/dpop.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/token_cache.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/connectors/registry.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/dx/decorators.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/events.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/exceptions.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/models/audit.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/py.typed +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/transport/__init__.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/transport/http.py +0 -0
- {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/vault.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-fabriq-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.25
|
|
4
4
|
Summary: Agentic Fabriq SDK: high-level client, CLI tool, DX helpers, and auth for AI agents
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: fabriq,agentic-fabriq,sdk,ai,agents,agentic,fabric,cli
|
|
@@ -88,15 +88,34 @@ async def main():
|
|
|
88
88
|
tools = await af.list_tools()
|
|
89
89
|
agents = await af.list_agents()
|
|
90
90
|
|
|
91
|
-
# Invoke tools
|
|
91
|
+
# Invoke Slack tools
|
|
92
92
|
result = await af.invoke_connection("my-slack", method="get_channels")
|
|
93
93
|
|
|
94
|
-
#
|
|
94
|
+
# Post a Slack message
|
|
95
95
|
await af.invoke_connection(
|
|
96
96
|
"my-slack",
|
|
97
97
|
method="post_message",
|
|
98
98
|
parameters={"channel": "test", "text": "Hello from SDK!"}
|
|
99
99
|
)
|
|
100
|
+
|
|
101
|
+
# Get Gmail emails
|
|
102
|
+
result = await af.invoke_connection(
|
|
103
|
+
"gmail_work",
|
|
104
|
+
method="get_emails",
|
|
105
|
+
parameters={"max_results": 10, "q": "is:unread"}
|
|
106
|
+
)
|
|
107
|
+
emails = result.get("emails", [])
|
|
108
|
+
|
|
109
|
+
# Send an email
|
|
110
|
+
await af.invoke_connection(
|
|
111
|
+
"gmail_work",
|
|
112
|
+
method="send_email",
|
|
113
|
+
parameters={
|
|
114
|
+
"to": "recipient@example.com",
|
|
115
|
+
"subject": "Hello from SDK",
|
|
116
|
+
"body": "This email was sent via Agentic Fabric SDK!"
|
|
117
|
+
}
|
|
118
|
+
)
|
|
100
119
|
```
|
|
101
120
|
|
|
102
121
|
DX orchestration:
|
|
@@ -52,15 +52,34 @@ async def main():
|
|
|
52
52
|
tools = await af.list_tools()
|
|
53
53
|
agents = await af.list_agents()
|
|
54
54
|
|
|
55
|
-
# Invoke tools
|
|
55
|
+
# Invoke Slack tools
|
|
56
56
|
result = await af.invoke_connection("my-slack", method="get_channels")
|
|
57
57
|
|
|
58
|
-
#
|
|
58
|
+
# Post a Slack message
|
|
59
59
|
await af.invoke_connection(
|
|
60
60
|
"my-slack",
|
|
61
61
|
method="post_message",
|
|
62
62
|
parameters={"channel": "test", "text": "Hello from SDK!"}
|
|
63
63
|
)
|
|
64
|
+
|
|
65
|
+
# Get Gmail emails
|
|
66
|
+
result = await af.invoke_connection(
|
|
67
|
+
"gmail_work",
|
|
68
|
+
method="get_emails",
|
|
69
|
+
parameters={"max_results": 10, "q": "is:unread"}
|
|
70
|
+
)
|
|
71
|
+
emails = result.get("emails", [])
|
|
72
|
+
|
|
73
|
+
# Send an email
|
|
74
|
+
await af.invoke_connection(
|
|
75
|
+
"gmail_work",
|
|
76
|
+
method="send_email",
|
|
77
|
+
parameters={
|
|
78
|
+
"to": "recipient@example.com",
|
|
79
|
+
"subject": "Hello from SDK",
|
|
80
|
+
"body": "This email was sent via Agentic Fabric SDK!"
|
|
81
|
+
}
|
|
82
|
+
)
|
|
64
83
|
```
|
|
65
84
|
|
|
66
85
|
DX orchestration:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agentic Fabric CLI Tool
|
|
3
|
+
|
|
4
|
+
A command-line interface for managing Agentic Fabric resources including
|
|
5
|
+
agents, tools, and administrative operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
10
|
+
try:
|
|
11
|
+
__version__ = version("agentic-fabriq-sdk")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
# Fallback if package is not installed
|
|
14
|
+
__version__ = "0.0.0-dev"
|
|
15
|
+
except ImportError:
|
|
16
|
+
# Fallback for Python < 3.8 (though we require 3.11+)
|
|
17
|
+
__version__ = "0.0.0-dev"
|
|
@@ -18,17 +18,21 @@ app = typer.Typer(help="Manage registered applications")
|
|
|
18
18
|
console = Console()
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
@app.command("
|
|
22
|
-
def
|
|
21
|
+
@app.command("register")
|
|
22
|
+
def register_application(
|
|
23
23
|
app_id: str = typer.Option(..., "--app-id", help="Application identifier (no spaces)"),
|
|
24
24
|
connections: str = typer.Option(..., "--connections", help="Tool connections (format: 'tool1:conn-id,tool2:conn-id')"),
|
|
25
25
|
scopes: Optional[str] = typer.Option(None, "--scopes", help="Scopes (format: 'scope1,scope2,scope3')"),
|
|
26
26
|
):
|
|
27
27
|
"""
|
|
28
|
-
Register a new application.
|
|
28
|
+
Step 1: Register a new application (returns activation token).
|
|
29
|
+
|
|
30
|
+
This registers your application and returns a temporary activation token
|
|
31
|
+
that expires in 1 hour. Use this token with 'afctl applications connect'
|
|
32
|
+
to complete the setup and save credentials locally.
|
|
29
33
|
|
|
30
34
|
Example:
|
|
31
|
-
afctl applications
|
|
35
|
+
afctl applications register \\
|
|
32
36
|
--app-id my-slack-bot \\
|
|
33
37
|
--connections slack:my-slack-conn,github:my-github-conn \\
|
|
34
38
|
--scopes slack:read,slack:write,github:repo:read
|
|
@@ -58,10 +62,10 @@ def create_application(
|
|
|
58
62
|
for conn_id in tool_connections:
|
|
59
63
|
tool_connections[conn_id] = scope_list
|
|
60
64
|
|
|
61
|
-
# Make API request
|
|
65
|
+
# Make API request to register (returns activation token)
|
|
62
66
|
try:
|
|
63
67
|
response = httpx.post(
|
|
64
|
-
f"{config.gateway_url}/api/v1/applications",
|
|
68
|
+
f"{config.gateway_url}/api/v1/applications/register",
|
|
65
69
|
headers={"Authorization": f"Bearer {config.access_token}"},
|
|
66
70
|
json={
|
|
67
71
|
"app_id": app_id,
|
|
@@ -83,6 +87,76 @@ def create_application(
|
|
|
83
87
|
|
|
84
88
|
data = response.json()
|
|
85
89
|
|
|
90
|
+
# Display activation token
|
|
91
|
+
console.print("\n✅ Application registered successfully!", style="green bold")
|
|
92
|
+
console.print(f"\n📋 App ID: {data['app_id']}", style="cyan")
|
|
93
|
+
console.print(f"\n🔑 Activation Token:", style="yellow bold")
|
|
94
|
+
console.print(f" {data['activation_token']}", style="yellow")
|
|
95
|
+
console.print(f"\n⏰ Token expires: {data['expires_at'][:19]} UTC", style="white")
|
|
96
|
+
console.print(f" (Valid for 1 hour)", style="dim")
|
|
97
|
+
|
|
98
|
+
console.print("\n📋 Next Steps:", style="cyan bold")
|
|
99
|
+
console.print(f" 1. Navigate to your project directory", style="white")
|
|
100
|
+
console.print(f" 2. Make sure you're authenticated: afctl auth login", style="white")
|
|
101
|
+
console.print(f" 3. Run the connect command:", style="white")
|
|
102
|
+
console.print(f"\n afctl applications connect {app_id} --token <activation-token>", style="green")
|
|
103
|
+
console.print(f"\n⚠️ Save the activation token! It expires in 1 hour and can only be used once.", style="yellow bold")
|
|
104
|
+
|
|
105
|
+
except httpx.HTTPError as e:
|
|
106
|
+
console.print(f"❌ Network error: {e}", style="red")
|
|
107
|
+
raise typer.Exit(1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command("connect")
|
|
111
|
+
def connect_application(
|
|
112
|
+
app_id: str = typer.Argument(..., help="Application identifier"),
|
|
113
|
+
token: str = typer.Option(..., "--token", help="Activation token from registration"),
|
|
114
|
+
):
|
|
115
|
+
"""
|
|
116
|
+
Step 2: Connect/activate an application (saves credentials locally).
|
|
117
|
+
|
|
118
|
+
Uses the activation token from 'afctl applications register' to activate
|
|
119
|
+
the application and save credentials to the current directory.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
afctl applications connect my-slack-bot --token <activation-token>
|
|
123
|
+
"""
|
|
124
|
+
config = get_config()
|
|
125
|
+
|
|
126
|
+
if not config.is_authenticated():
|
|
127
|
+
console.print("❌ Not authenticated. Run 'afctl auth login' first.", style="red")
|
|
128
|
+
raise typer.Exit(1)
|
|
129
|
+
|
|
130
|
+
# Make API request to activate (returns final credentials)
|
|
131
|
+
try:
|
|
132
|
+
response = httpx.post(
|
|
133
|
+
f"{config.gateway_url}/api/v1/applications/activate",
|
|
134
|
+
headers={"Authorization": f"Bearer {config.access_token}"},
|
|
135
|
+
json={"activation_token": token},
|
|
136
|
+
timeout=30.0
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if response.status_code == 404:
|
|
140
|
+
console.print("❌ Invalid or expired activation token", style="red")
|
|
141
|
+
console.print(" The token may have expired (valid for 1 hour) or was already used.", style="yellow")
|
|
142
|
+
console.print(" Register again with 'afctl applications register'", style="white")
|
|
143
|
+
raise typer.Exit(1)
|
|
144
|
+
elif response.status_code == 403:
|
|
145
|
+
console.print("❌ This activation token does not belong to you", style="red")
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
elif response.status_code != 201:
|
|
148
|
+
error_detail = response.text
|
|
149
|
+
try:
|
|
150
|
+
error_json = response.json()
|
|
151
|
+
error_detail = error_json.get("detail", response.text)
|
|
152
|
+
except:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
console.print(f"❌ Failed to activate application: {error_detail}", style="red")
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
|
|
158
|
+
data = response.json()
|
|
159
|
+
|
|
86
160
|
# Save credentials locally
|
|
87
161
|
from af_sdk import save_application_config
|
|
88
162
|
|
|
@@ -99,7 +173,7 @@ def create_application(
|
|
|
99
173
|
app_file = save_application_config(data["app_id"], app_config)
|
|
100
174
|
|
|
101
175
|
# Display success
|
|
102
|
-
console.print("\n✅ Application
|
|
176
|
+
console.print("\n✅ Application activated successfully!", style="green bold")
|
|
103
177
|
console.print(f"\n📋 App ID: {data['app_id']}", style="cyan")
|
|
104
178
|
console.print(f"🔑 Secret Key: {data['secret_key']}", style="yellow")
|
|
105
179
|
console.print(f"\n💾 Credentials saved to: {app_file}", style="green")
|
|
@@ -116,24 +190,59 @@ def create_application(
|
|
|
116
190
|
@app.command("list")
|
|
117
191
|
def list_applications(
|
|
118
192
|
format: str = typer.Option("table", "--format", help="Output format (table, json, yaml)"),
|
|
193
|
+
sync: bool = typer.Option(True, "--sync/--no-sync", help="Sync with server and remove orphaned local files"),
|
|
119
194
|
):
|
|
120
195
|
"""
|
|
121
196
|
List all registered applications.
|
|
122
197
|
|
|
123
|
-
Shows applications from
|
|
124
|
-
|
|
125
|
-
|
|
198
|
+
Shows applications from local config files (~/.af/applications/) and optionally
|
|
199
|
+
syncs with the server to remove any local files for applications that have been
|
|
200
|
+
deleted from the server (e.g., via the UI).
|
|
126
201
|
"""
|
|
127
202
|
config = get_config()
|
|
128
203
|
|
|
129
204
|
# Load from local config first
|
|
130
|
-
from af_sdk import list_applications as list_local_apps
|
|
205
|
+
from af_sdk import list_applications as list_local_apps, delete_application_config
|
|
131
206
|
|
|
132
207
|
local_apps = list_local_apps()
|
|
133
208
|
|
|
209
|
+
# If sync is enabled and user is authenticated, check server and clean up orphans
|
|
210
|
+
if sync and config.is_authenticated():
|
|
211
|
+
try:
|
|
212
|
+
response = httpx.get(
|
|
213
|
+
f"{config.gateway_url}/api/v1/applications",
|
|
214
|
+
headers={"Authorization": f"Bearer {config.access_token}"},
|
|
215
|
+
timeout=30.0
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if response.status_code == 200:
|
|
219
|
+
data = response.json()
|
|
220
|
+
server_apps = data.get("applications", [])
|
|
221
|
+
server_app_ids = {app["app_id"] for app in server_apps}
|
|
222
|
+
|
|
223
|
+
# Find and remove orphaned local files
|
|
224
|
+
orphaned = []
|
|
225
|
+
for local_app in local_apps[:]: # Copy list to modify during iteration
|
|
226
|
+
if local_app["app_id"] not in server_app_ids:
|
|
227
|
+
orphaned.append(local_app["app_id"])
|
|
228
|
+
# Delete orphaned local config
|
|
229
|
+
delete_application_config(local_app["app_id"])
|
|
230
|
+
# Remove from local_apps list
|
|
231
|
+
local_apps.remove(local_app)
|
|
232
|
+
|
|
233
|
+
if orphaned:
|
|
234
|
+
console.print(f"🧹 Cleaned up {len(orphaned)} orphaned local file(s): {', '.join(orphaned)}", style="yellow")
|
|
235
|
+
|
|
236
|
+
except httpx.HTTPError as e:
|
|
237
|
+
# If server check fails, just show local apps with a warning
|
|
238
|
+
console.print(f"⚠️ Could not sync with server: {e}", style="yellow")
|
|
239
|
+
except Exception as e:
|
|
240
|
+
# Silently continue if sync fails
|
|
241
|
+
pass
|
|
242
|
+
|
|
134
243
|
if format == "table":
|
|
135
244
|
if not local_apps:
|
|
136
|
-
console.print("No applications registered
|
|
245
|
+
console.print("No applications registered.", style="yellow")
|
|
137
246
|
return
|
|
138
247
|
|
|
139
248
|
table = Table(title="Registered Applications")
|
|
@@ -53,15 +53,16 @@ def login(
|
|
|
53
53
|
"--tenant-id",
|
|
54
54
|
help="Tenant ID (optional, can be extracted from JWT)"
|
|
55
55
|
),
|
|
56
|
-
browser: bool = typer.Option(
|
|
57
|
-
True,
|
|
58
|
-
help="Open browser for authentication"
|
|
59
|
-
),
|
|
60
56
|
keycloak_url: Optional[str] = typer.Option(
|
|
61
57
|
None,
|
|
62
58
|
"--keycloak-url",
|
|
63
59
|
help="Keycloak URL (default: https://auth.agenticfabriq.com or from config)"
|
|
64
60
|
),
|
|
61
|
+
yes: bool = typer.Option(
|
|
62
|
+
False,
|
|
63
|
+
"--yes",
|
|
64
|
+
help="Skip confirmation when already authenticated"
|
|
65
|
+
),
|
|
65
66
|
):
|
|
66
67
|
"""
|
|
67
68
|
Login to Agentic Fabric using OAuth2/PKCE flow.
|
|
@@ -80,7 +81,7 @@ def login(
|
|
|
80
81
|
info(f"Already authenticated as: {user_display}")
|
|
81
82
|
info(f"Tenant: {existing_token.tenant_id or 'Unknown'}")
|
|
82
83
|
|
|
83
|
-
if not typer.confirm("Do you want to login again?"):
|
|
84
|
+
if not yes and not typer.confirm("Do you want to login again?"):
|
|
84
85
|
return
|
|
85
86
|
|
|
86
87
|
try:
|
|
@@ -89,8 +90,8 @@ def login(
|
|
|
89
90
|
|
|
90
91
|
# Perform login
|
|
91
92
|
console.print()
|
|
92
|
-
#
|
|
93
|
-
tokens = oauth_client.login(open_browser=
|
|
93
|
+
# Always open browser for authentication
|
|
94
|
+
tokens = oauth_client.login(open_browser=True, timeout=300, use_hosted_callback=False)
|
|
94
95
|
|
|
95
96
|
# Extract and save token data
|
|
96
97
|
token_data = token_storage.extract_token_info(tokens)
|
|
@@ -12,12 +12,14 @@ app = typer.Typer(help="Configuration commands")
|
|
|
12
12
|
|
|
13
13
|
@app.command()
|
|
14
14
|
def show(
|
|
15
|
-
format: str = typer.Option(
|
|
15
|
+
format: str = typer.Option(None, "--format", help="Output format (overrides configured default)"),
|
|
16
16
|
):
|
|
17
17
|
"""Show current configuration."""
|
|
18
|
-
import os
|
|
19
18
|
config = get_config()
|
|
20
19
|
|
|
20
|
+
# Use provided format, or fall back to configured output_format
|
|
21
|
+
display_format = format if format else config.output_format
|
|
22
|
+
|
|
21
23
|
config_data = {
|
|
22
24
|
"gateway_url": config.gateway_url,
|
|
23
25
|
"keycloak_url": config.keycloak_url,
|
|
@@ -25,13 +27,12 @@ def show(
|
|
|
25
27
|
"authenticated": "Yes" if config.is_authenticated() else "No",
|
|
26
28
|
"config_file": config.config_file,
|
|
27
29
|
"output_format": config.output_format,
|
|
28
|
-
"
|
|
29
|
-
"env_keycloak_url": os.getenv("AF_KEYCLOAK_URL", "Not set"),
|
|
30
|
+
"page_size": config.page_size,
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
print_output(
|
|
33
34
|
config_data,
|
|
34
|
-
format_type=
|
|
35
|
+
format_type=display_format,
|
|
35
36
|
title="Configuration"
|
|
36
37
|
)
|
|
37
38
|
|
|
@@ -44,15 +45,20 @@ def set(
|
|
|
44
45
|
"""Set configuration value."""
|
|
45
46
|
config = get_config()
|
|
46
47
|
|
|
48
|
+
# Note: tenant_id is not settable - it comes from authentication
|
|
47
49
|
valid_keys = {
|
|
48
50
|
"gateway_url": "gateway_url",
|
|
49
|
-
"
|
|
51
|
+
"keycloak_url": "keycloak_url",
|
|
50
52
|
"output_format": "output_format",
|
|
53
|
+
"page_size": "page_size",
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
if key not in valid_keys:
|
|
54
57
|
error(f"Invalid configuration key: {key}")
|
|
55
58
|
error(f"Valid keys: {', '.join(valid_keys.keys())}")
|
|
59
|
+
if key == "tenant_id":
|
|
60
|
+
error("Note: tenant_id cannot be set manually. It comes from authentication.")
|
|
61
|
+
error("Run 'afctl auth login' to authenticate with a specific tenant.")
|
|
56
62
|
raise typer.Exit(1)
|
|
57
63
|
|
|
58
64
|
# Set the value
|
|
@@ -69,27 +75,46 @@ def get(
|
|
|
69
75
|
"""Get configuration value."""
|
|
70
76
|
config = get_config()
|
|
71
77
|
|
|
78
|
+
# All readable config keys
|
|
72
79
|
valid_keys = {
|
|
73
80
|
"gateway_url": "gateway_url",
|
|
81
|
+
"keycloak_url": "keycloak_url",
|
|
82
|
+
"keycloak_realm": "keycloak_realm",
|
|
83
|
+
"keycloak_client_id": "keycloak_client_id",
|
|
74
84
|
"tenant_id": "tenant_id",
|
|
75
85
|
"output_format": "output_format",
|
|
86
|
+
"page_size": "page_size",
|
|
87
|
+
"config_file": "config_file",
|
|
88
|
+
"verbose": "verbose",
|
|
89
|
+
"authenticated": "is_authenticated", # Special: calls method
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
if key not in valid_keys:
|
|
79
93
|
error(f"Invalid configuration key: {key}")
|
|
80
|
-
error(f"Valid keys: {', '.join(valid_keys.keys())}")
|
|
94
|
+
error(f"Valid keys: {', '.join(sorted(valid_keys.keys()))}")
|
|
81
95
|
raise typer.Exit(1)
|
|
82
96
|
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
# Handle special keys that are methods
|
|
98
|
+
if key == "authenticated":
|
|
99
|
+
value = "Yes" if config.is_authenticated() else "No"
|
|
100
|
+
else:
|
|
101
|
+
value = getattr(config, valid_keys[key])
|
|
102
|
+
|
|
103
|
+
# Display value
|
|
104
|
+
if value is None:
|
|
105
|
+
info(f"{key}: (not set)")
|
|
106
|
+
else:
|
|
107
|
+
info(f"{key}: {value}")
|
|
85
108
|
|
|
86
109
|
|
|
87
110
|
@app.command()
|
|
88
|
-
def reset(
|
|
111
|
+
def reset(
|
|
112
|
+
yes: bool = typer.Option(False, "--yes", help="Skip confirmation"),
|
|
113
|
+
):
|
|
89
114
|
"""Reset configuration to defaults."""
|
|
90
115
|
config = get_config()
|
|
91
116
|
|
|
92
|
-
if not typer.confirm("Are you sure you want to reset configuration to defaults?"):
|
|
117
|
+
if not yes and not typer.confirm("Are you sure you want to reset configuration to defaults?"):
|
|
93
118
|
info("Reset cancelled")
|
|
94
119
|
return
|
|
95
120
|
|
|
@@ -100,6 +125,7 @@ def reset():
|
|
|
100
125
|
config.gateway_url = "https://dashboard.agenticfabriq.com"
|
|
101
126
|
config.tenant_id = None
|
|
102
127
|
config.output_format = "table"
|
|
128
|
+
config.page_size = 20
|
|
103
129
|
|
|
104
130
|
config.save()
|
|
105
131
|
|
|
@@ -14,14 +14,105 @@ app = typer.Typer(help="Tool management commands")
|
|
|
14
14
|
@app.command()
|
|
15
15
|
def list(
|
|
16
16
|
format: str = typer.Option("table", "--format", help="Output format"),
|
|
17
|
+
page: int = typer.Option(1, "--page", help="Page number (starts from 1)", min=1),
|
|
18
|
+
page_size: int = typer.Option(None, "--page-size", help="Number of items per page (1-100). When specified, becomes the new default.", min=1, max=100),
|
|
19
|
+
search: str = typer.Option(None, "--search", help="Search query (searches tool IDs from registry like 'gmail', 'slack', and user connection names)"),
|
|
20
|
+
tool_filter: str = typer.Option(None, "--tool", help="Filter by tool type from registry (e.g., 'gmail' shows Gmail connections, 'google' shows all Google tools)"),
|
|
17
21
|
):
|
|
18
|
-
"""List your tool connections (configured and connected tools).
|
|
22
|
+
"""List your tool connections (configured and connected tools).
|
|
23
|
+
|
|
24
|
+
The command supports pagination and search to help you navigate large numbers
|
|
25
|
+
of connections efficiently.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
# List all connections (default: page 1, 20 per page)
|
|
29
|
+
afctl tools list
|
|
30
|
+
|
|
31
|
+
# List first page with 10 items per page
|
|
32
|
+
afctl tools list --page 1 --page-size 10
|
|
33
|
+
|
|
34
|
+
# Search for tool types (exact match against registry - shows all connections of that type)
|
|
35
|
+
afctl tools list --search gmail # Shows all Gmail connections
|
|
36
|
+
afctl tools list --search google # Shows all Google Workspace tools
|
|
37
|
+
afctl tools list --search slack # Shows all Slack connections
|
|
38
|
+
afctl tools list --search notion # Shows all Notion connections
|
|
39
|
+
afctl tools list --search github # Shows all GitHub connections
|
|
40
|
+
|
|
41
|
+
# Filter by tool type (shows all connections of that tool)
|
|
42
|
+
afctl tools list --tool gmail # Shows all Gmail connections
|
|
43
|
+
afctl tools list --tool google # Shows all Google Workspace tools
|
|
44
|
+
afctl tools list --tool slack # Shows all Slack connections
|
|
45
|
+
|
|
46
|
+
# Search and paginate together
|
|
47
|
+
afctl tools list --search google --page 1 --page-size 5
|
|
48
|
+
|
|
49
|
+
# Find connections by name (fallback search)
|
|
50
|
+
afctl tools list --search work # Connections with "work" in name
|
|
51
|
+
afctl tools list --search personal # Connections with "personal" in name
|
|
52
|
+
|
|
53
|
+
# Combined search and filtering (AND logic - must match both)
|
|
54
|
+
afctl tools list --tool google --search gmail # Google tools containing "gmail"
|
|
55
|
+
afctl tools list --search drive --tool google # Google tools with "drive"
|
|
56
|
+
afctl tools list --search team --tool slack # Slack connections with "team"
|
|
57
|
+
|
|
58
|
+
Pagination:
|
|
59
|
+
- Pages start from 1
|
|
60
|
+
- Page size range: 1-100 items
|
|
61
|
+
- Shows helpful tips when more results are available
|
|
62
|
+
|
|
63
|
+
Search:
|
|
64
|
+
- **Primary**: Searches available tool IDs from registry (e.g., "gmail" matches "gmail", "google" matches "google_drive")
|
|
65
|
+
- **Secondary**: Searches user connection IDs and display names (e.g., "gmail" matches "gmail_work")
|
|
66
|
+
- Case-insensitive matching
|
|
67
|
+
- Shows all connections matching the search criteria
|
|
68
|
+
|
|
69
|
+
Tool Filtering:
|
|
70
|
+
- Filters by tool type from connector registry
|
|
71
|
+
- Shows all user connections for that tool type
|
|
72
|
+
- "google" shows all Google Workspace tools (Drive, Docs, Sheets, Gmail, etc.)
|
|
73
|
+
- "gmail" shows only Gmail connections
|
|
74
|
+
- "slack" shows only Slack connections
|
|
75
|
+
- Combines with search and pagination
|
|
76
|
+
"""
|
|
19
77
|
try:
|
|
78
|
+
from af_cli.core.config import get_config
|
|
79
|
+
config = get_config()
|
|
80
|
+
|
|
81
|
+
# Use provided page_size, or fall back to configured default
|
|
82
|
+
if page_size is None:
|
|
83
|
+
page_size = config.page_size
|
|
84
|
+
else:
|
|
85
|
+
# Save the new page_size as default
|
|
86
|
+
config.page_size = page_size
|
|
87
|
+
config.save()
|
|
88
|
+
|
|
20
89
|
with get_client() as client:
|
|
21
|
-
|
|
90
|
+
# Build query parameters - always include all params for clarity
|
|
91
|
+
params = {
|
|
92
|
+
"page": page,
|
|
93
|
+
"page_size": page_size,
|
|
94
|
+
}
|
|
95
|
+
if search:
|
|
96
|
+
params["search"] = search
|
|
97
|
+
if tool_filter:
|
|
98
|
+
params["tool_filter"] = tool_filter
|
|
22
99
|
|
|
100
|
+
debug(f"Requesting connections with params: {params}")
|
|
101
|
+
connections = client.get("/api/v1/user-connections", params=params)
|
|
102
|
+
|
|
103
|
+
debug(f"Received {len(connections) if connections else 0} connections from API")
|
|
104
|
+
|
|
23
105
|
if not connections:
|
|
24
|
-
|
|
106
|
+
if page > 1:
|
|
107
|
+
# User requested a page beyond available data
|
|
108
|
+
error(f"Page {page} is out of range.")
|
|
109
|
+
info(f"Try 'afctl tools list --page 1' to see available connections.")
|
|
110
|
+
if search or tool_filter:
|
|
111
|
+
info(f"Or adjust your search/filter criteria to see more results.")
|
|
112
|
+
elif search or tool_filter:
|
|
113
|
+
warning(f"No tool connections found matching your criteria. Try adjusting your search or filter.")
|
|
114
|
+
else:
|
|
115
|
+
warning("No tool connections found. Add connections in the dashboard UI.")
|
|
25
116
|
return
|
|
26
117
|
|
|
27
118
|
# Format for better display
|
|
@@ -42,11 +133,37 @@ def list(
|
|
|
42
133
|
"Added": conn.get("created_at", "N/A")[:10] if conn.get("created_at") else "N/A",
|
|
43
134
|
})
|
|
44
135
|
|
|
136
|
+
# Show pagination and filter info
|
|
137
|
+
total_info = ""
|
|
138
|
+
if page != 1 or page_size != 20 or search or tool_filter:
|
|
139
|
+
total_info = f" (Page {page}, {len(connections)} shown"
|
|
140
|
+
if search:
|
|
141
|
+
total_info += f", Search: '{search}'"
|
|
142
|
+
if tool_filter:
|
|
143
|
+
total_info += f", Tool: '{tool_filter}'"
|
|
144
|
+
total_info += ")"
|
|
145
|
+
|
|
146
|
+
# Add summary info if filters are active
|
|
147
|
+
if search or tool_filter:
|
|
148
|
+
filter_parts = []
|
|
149
|
+
if search:
|
|
150
|
+
filter_parts.append(f"search='{search}'")
|
|
151
|
+
if tool_filter:
|
|
152
|
+
filter_parts.append(f"tool='{tool_filter}'")
|
|
153
|
+
info(f"🔍 Filtered results: {' AND '.join(filter_parts)}")
|
|
154
|
+
|
|
45
155
|
print_output(
|
|
46
156
|
display_data,
|
|
47
157
|
format_type=format,
|
|
48
|
-
title="Your Tool Connections"
|
|
158
|
+
title=f"Your Tool Connections{total_info}"
|
|
49
159
|
)
|
|
160
|
+
|
|
161
|
+
# Show helpful tips for pagination
|
|
162
|
+
if len(connections) == page_size:
|
|
163
|
+
info(f"💡 Showing {len(connections)} results. Use --page {page + 1} to see more results")
|
|
164
|
+
|
|
165
|
+
if not search and not tool_filter:
|
|
166
|
+
info(f"💡 Tip: Use --search <term> to search, or --tool <type> to filter by tool type")
|
|
50
167
|
|
|
51
168
|
except Exception as e:
|
|
52
169
|
error(f"Failed to list tool connections: {e}")
|
|
@@ -215,6 +332,12 @@ def add(
|
|
|
215
332
|
# Google (oauth3 method - uses platform OAuth, no credentials needed)
|
|
216
333
|
afctl tools add google_drive --connection-id google-work --method oauth3
|
|
217
334
|
|
|
335
|
+
# Slack (oauth3 method - uses platform OAuth, no credentials needed)
|
|
336
|
+
afctl tools add slack --connection-id slack-work --method oauth3
|
|
337
|
+
|
|
338
|
+
# Notion (oauth3 method - uses platform OAuth, no credentials needed)
|
|
339
|
+
afctl tools add notion --connection-id notion-work --method oauth3
|
|
340
|
+
|
|
218
341
|
# Google (api_credentials method - your own OAuth app)
|
|
219
342
|
afctl tools add google_drive --connection-id google-work --method api_credentials \\
|
|
220
343
|
--client-id "123.apps.googleusercontent.com" \\
|
|
@@ -254,10 +377,10 @@ def add(
|
|
|
254
377
|
error("Method must be 'api_credentials', 'oauth3', or 'oauth'")
|
|
255
378
|
raise typer.Exit(1)
|
|
256
379
|
|
|
257
|
-
# Validate oauth3 method is only for Google tools
|
|
380
|
+
# Validate oauth3 method is only for Google, Slack, and Notion tools
|
|
258
381
|
if method == "oauth3":
|
|
259
|
-
if not (tool.startswith("google_") or tool == "gmail"):
|
|
260
|
-
error("oauth3 method is only available for Google Workspace tools")
|
|
382
|
+
if not (tool.startswith("google_") or tool == "gmail" or tool == "slack" or tool == "notion"):
|
|
383
|
+
error("oauth3 method is only available for Google Workspace tools, Slack, and Notion")
|
|
261
384
|
info("For other tools, use 'api_credentials' method")
|
|
262
385
|
raise typer.Exit(1)
|
|
263
386
|
|
|
@@ -297,7 +420,7 @@ def add(
|
|
|
297
420
|
# OAuth3 uses platform credentials - no need to store user credentials
|
|
298
421
|
success("✅ Connection configured with platform OAuth")
|
|
299
422
|
info("")
|
|
300
|
-
info(f"Next: Run 'afctl tools connect {connection_id}' to authenticate
|
|
423
|
+
info(f"Next: Run 'afctl tools connect {connection_id}' to authenticate")
|
|
301
424
|
|
|
302
425
|
elif method == "api_credentials":
|
|
303
426
|
# Determine the API base tool name (Google tools all use "google")
|
|
@@ -369,6 +492,7 @@ def add(
|
|
|
369
492
|
@app.command()
|
|
370
493
|
def connect(
|
|
371
494
|
connection_id: str = typer.Argument(..., help="Connection ID to connect"),
|
|
495
|
+
yes: bool = typer.Option(False, "--yes", help="Skip confirmation when reconnecting"),
|
|
372
496
|
):
|
|
373
497
|
"""Complete OAuth connection (open browser for authorization)."""
|
|
374
498
|
try:
|
|
@@ -396,18 +520,26 @@ def connect(
|
|
|
396
520
|
# Check if connection is already set up (has credentials stored)
|
|
397
521
|
if connection.get("connected"):
|
|
398
522
|
warning(f"Connection '{connection_id}' is already connected")
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
523
|
+
if not yes:
|
|
524
|
+
confirm = typer.confirm("Do you want to reconnect (re-authorize)?")
|
|
525
|
+
if not confirm:
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
# Determine the API base tool name
|
|
529
|
+
# - Google tools all use "google" for API credentials, "google_oauth" for oauth3
|
|
530
|
+
# - Notion uses "notion" for API credentials, "notion_oauth" for oauth3
|
|
531
|
+
if tool.startswith("google_") or tool == "gmail":
|
|
532
|
+
api_tool_name = "google_oauth" if method == "oauth3" else "google"
|
|
533
|
+
elif tool == "notion":
|
|
534
|
+
api_tool_name = "notion_oauth" if method == "oauth3" else "notion"
|
|
535
|
+
else:
|
|
536
|
+
api_tool_name = tool
|
|
405
537
|
|
|
406
538
|
# Initiate OAuth flow
|
|
407
539
|
info(f"Initiating OAuth for {tool}...")
|
|
408
540
|
|
|
409
541
|
# For Google tools, pass the specific tool_type parameter
|
|
410
|
-
tool_type_param = f"&tool_type={tool}" if tool
|
|
542
|
+
tool_type_param = f"&tool_type={tool}" if (tool.startswith("google_") or tool == "gmail") else ""
|
|
411
543
|
|
|
412
544
|
# For oauth3 method, pass the method parameter to use platform credentials
|
|
413
545
|
method_param = f"&method={method}" if method == "oauth3" else ""
|
|
@@ -63,7 +63,12 @@ class AFClient:
|
|
|
63
63
|
def get(self, path: str, params: Optional[Dict] = None) -> Dict[str, Any]:
|
|
64
64
|
"""Make GET request."""
|
|
65
65
|
url = urljoin(self.config.gateway_url, path)
|
|
66
|
-
|
|
66
|
+
if params:
|
|
67
|
+
# Show params in debug output
|
|
68
|
+
param_str = "&".join(f"{k}={v}" for k, v in params.items())
|
|
69
|
+
debug(f"GET {url}?{param_str}")
|
|
70
|
+
else:
|
|
71
|
+
debug(f"GET {url}")
|
|
67
72
|
|
|
68
73
|
response = self.client.get(
|
|
69
74
|
path,
|
|
@@ -13,7 +13,7 @@ from pydantic import BaseModel, Field
|
|
|
13
13
|
class CLIConfig(BaseModel):
|
|
14
14
|
"""CLI configuration model."""
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# Gateway settings (support environment variable override)
|
|
17
17
|
gateway_url: str = Field(
|
|
18
18
|
default_factory=lambda: os.getenv("AF_GATEWAY_URL", "https://dashboard.agenticfabriq.com"),
|
|
19
19
|
description="Gateway URL"
|
|
@@ -39,6 +39,7 @@ class CLIConfig(BaseModel):
|
|
|
39
39
|
config_file: str = Field(default="", description="Configuration file path")
|
|
40
40
|
verbose: bool = Field(default=False, description="Verbose output")
|
|
41
41
|
output_format: str = Field(default="table", description="Output format (table, json, yaml)")
|
|
42
|
+
page_size: int = Field(default=20, description="Default page size for list commands")
|
|
42
43
|
|
|
43
44
|
def __init__(self, **data):
|
|
44
45
|
super().__init__(**data)
|
|
@@ -12,8 +12,6 @@ from rich.table import Table
|
|
|
12
12
|
from af_cli.commands.applications import app as applications_app
|
|
13
13
|
from af_cli.commands.auth import app as auth_app
|
|
14
14
|
from af_cli.commands.config import app as config_app
|
|
15
|
-
from af_cli.commands.mcp_servers import app as mcp_servers_app
|
|
16
|
-
from af_cli.commands.secrets import app as secrets_app
|
|
17
15
|
from af_cli.commands.tools import app as tools_app
|
|
18
16
|
from af_cli.core.config import get_config
|
|
19
17
|
from af_cli.core.output import success, error, info
|
|
@@ -31,8 +29,6 @@ app.add_typer(auth_app, name="auth", help="Authentication commands")
|
|
|
31
29
|
app.add_typer(config_app, name="config", help="Configuration commands")
|
|
32
30
|
app.add_typer(tools_app, name="tools", help="Tool management commands")
|
|
33
31
|
app.add_typer(applications_app, name="applications", help="Application management commands")
|
|
34
|
-
app.add_typer(mcp_servers_app, name="mcp-servers", help="MCP server management commands")
|
|
35
|
-
app.add_typer(secrets_app, name="secrets", help="Secret management commands")
|
|
36
32
|
|
|
37
33
|
|
|
38
34
|
@app.command()
|
|
@@ -92,19 +88,19 @@ def init(
|
|
|
92
88
|
"--tenant-id",
|
|
93
89
|
help="Tenant ID"
|
|
94
90
|
),
|
|
95
|
-
|
|
91
|
+
yes: bool = typer.Option(
|
|
96
92
|
False,
|
|
97
|
-
"--
|
|
98
|
-
help="
|
|
93
|
+
"--yes",
|
|
94
|
+
help="Skip confirmation and overwrite existing config"
|
|
99
95
|
),
|
|
100
96
|
):
|
|
101
97
|
"""Initialize CLI configuration."""
|
|
102
98
|
config = get_config()
|
|
103
99
|
|
|
104
100
|
# Check if config exists
|
|
105
|
-
if os.path.exists(config.config_file) and not
|
|
101
|
+
if os.path.exists(config.config_file) and not yes:
|
|
106
102
|
error(f"Configuration already exists at {config.config_file}")
|
|
107
|
-
error("Use --
|
|
103
|
+
error("Use --yes to overwrite")
|
|
108
104
|
raise typer.Exit(1)
|
|
109
105
|
|
|
110
106
|
# Create config directory if it doesn't exist
|
|
@@ -152,7 +148,7 @@ def main(
|
|
|
152
148
|
"""
|
|
153
149
|
Agentic Fabric CLI - Manage your connectivity hub.
|
|
154
150
|
|
|
155
|
-
The CLI provides commands for managing
|
|
151
|
+
The CLI provides commands for managing tool connections and applications
|
|
156
152
|
in your Agentic Fabric deployment.
|
|
157
153
|
"""
|
|
158
154
|
# Configure global options
|
|
@@ -47,6 +47,9 @@ def oauth_required(*, scopes: List[str], refresh_if_expired: bool = True):
|
|
|
47
47
|
refresh_if_expired=refresh_if_expired,
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
+
# Log token info (first 20 chars for debugging)
|
|
51
|
+
ctx.logger.info(f"OAuth decorator injecting token for {tool_id}: {token[:20]}...")
|
|
52
|
+
|
|
50
53
|
# Inject Authorization header
|
|
51
54
|
headers = kwargs.setdefault("headers", {})
|
|
52
55
|
headers.setdefault("Authorization", f"Bearer {token}")
|
|
@@ -7,7 +7,6 @@ from .base import (
|
|
|
7
7
|
BaseConnector,
|
|
8
8
|
ConnectorContext,
|
|
9
9
|
HTTPConnectorMixin,
|
|
10
|
-
MCPConnector,
|
|
11
10
|
ToolConnector,
|
|
12
11
|
)
|
|
13
12
|
from .registry import ConnectorRegistry
|
|
@@ -16,7 +15,6 @@ __all__ = [
|
|
|
16
15
|
"BaseConnector",
|
|
17
16
|
"ToolConnector",
|
|
18
17
|
"AgentConnector",
|
|
19
|
-
"MCPConnector",
|
|
20
18
|
"ConnectorContext",
|
|
21
19
|
"HTTPConnectorMixin",
|
|
22
20
|
"ConnectorRegistry",
|
|
@@ -105,7 +105,7 @@ class ToolConnector(BaseConnector):
|
|
|
105
105
|
|
|
106
106
|
|
|
107
107
|
class AgentConnector(BaseConnector):
|
|
108
|
-
"""Base class for agent connectors that speak A2A
|
|
108
|
+
"""Base class for agent connectors that speak A2A."""
|
|
109
109
|
|
|
110
110
|
AGENT_ID: str = ""
|
|
111
111
|
|
|
@@ -144,47 +144,6 @@ class AgentConnector(BaseConnector):
|
|
|
144
144
|
return True
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
class MCPConnector(BaseConnector):
|
|
148
|
-
"""Base class for MCP (Model Context Protocol) connectors."""
|
|
149
|
-
|
|
150
|
-
MCP_VERSION: str = "1.0"
|
|
151
|
-
SERVER_NAME: str = ""
|
|
152
|
-
|
|
153
|
-
def __init__(self, ctx: ConnectorContext):
|
|
154
|
-
super().__init__(ctx)
|
|
155
|
-
if not self.SERVER_NAME:
|
|
156
|
-
raise ConnectorError("SERVER_NAME must be set in MCP connector subclass")
|
|
157
|
-
|
|
158
|
-
@abc.abstractmethod
|
|
159
|
-
async def list_tools(self) -> list[Dict[str, Any]]:
|
|
160
|
-
"""List available tools from the MCP server."""
|
|
161
|
-
pass
|
|
162
|
-
|
|
163
|
-
@abc.abstractmethod
|
|
164
|
-
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
|
165
|
-
"""Call a specific tool on the MCP server."""
|
|
166
|
-
pass
|
|
167
|
-
|
|
168
|
-
async def invoke(self, method: str, **kwargs) -> Any:
|
|
169
|
-
"""Generic invoke method for MCP connectors."""
|
|
170
|
-
if method == "list_tools":
|
|
171
|
-
return await self.list_tools()
|
|
172
|
-
elif method == "call_tool":
|
|
173
|
-
return await self.call_tool(
|
|
174
|
-
kwargs.get("tool_name", ""), kwargs.get("arguments", {})
|
|
175
|
-
)
|
|
176
|
-
else:
|
|
177
|
-
raise ConnectorError(f"Unknown method: {method}")
|
|
178
|
-
|
|
179
|
-
async def get_server_info(self) -> Dict[str, Any]:
|
|
180
|
-
"""Get MCP server information."""
|
|
181
|
-
return {
|
|
182
|
-
"name": self.SERVER_NAME,
|
|
183
|
-
"version": self.MCP_VERSION,
|
|
184
|
-
"tools": await self.list_tools(),
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
147
|
class HTTPConnectorMixin:
|
|
189
148
|
"""Mixin for connectors that make HTTP requests."""
|
|
190
149
|
|
|
@@ -204,10 +163,18 @@ class HTTPConnectorMixin:
|
|
|
204
163
|
url = f"{self.base_url.rstrip('/')}/{path.lstrip('/')}"
|
|
205
164
|
request_headers = {**self.default_headers, **(headers or {})}
|
|
206
165
|
|
|
166
|
+
# Log request details for debugging
|
|
167
|
+
auth_header = request_headers.get("Authorization", "")
|
|
168
|
+
auth_preview = f"{auth_header[:30]}..." if auth_header else "None"
|
|
169
|
+
self.logger.info(f"[HTTP Request] {method} {url}")
|
|
170
|
+
self.logger.info(f"[HTTP Request] Authorization: {auth_preview}")
|
|
171
|
+
self.logger.info(f"[HTTP Request] Headers: {list(request_headers.keys())}")
|
|
172
|
+
|
|
207
173
|
try:
|
|
208
174
|
response = await self.session.request(
|
|
209
175
|
method, url, headers=request_headers, **kwargs
|
|
210
176
|
)
|
|
177
|
+
self.logger.info(f"[HTTP Response] Status: {response.status_code}")
|
|
211
178
|
response.raise_for_status()
|
|
212
179
|
return response
|
|
213
180
|
except httpx.HTTPError as e:
|
|
@@ -40,25 +40,3 @@ class ToolFabric:
|
|
|
40
40
|
r.raise_for_status()
|
|
41
41
|
return r.json()
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
class MCPServer:
|
|
45
|
-
"""Facade for an MCP server registered in Fabriq (proxy layer)."""
|
|
46
|
-
|
|
47
|
-
def __init__(self, *, server_id: str, base_url: str, access_token: str, tenant_id: Optional[str] = None):
|
|
48
|
-
self.server_id = server_id
|
|
49
|
-
self.base_url = base_url.rstrip("/")
|
|
50
|
-
self.token = access_token
|
|
51
|
-
self.tenant_id = tenant_id
|
|
52
|
-
|
|
53
|
-
def get_tools(self, names: List[str]) -> List[str]:
|
|
54
|
-
return [f"mcp:{self.server_id}:{name}" for name in names]
|
|
55
|
-
|
|
56
|
-
def invoke(self, tool: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
57
|
-
url = f"{self.base_url}/api/v1/proxy/mcp/{self.server_id}/invoke"
|
|
58
|
-
payload = {"payload": {"tool": tool, "args": args}}
|
|
59
|
-
with httpx.Client(timeout=60.0) as c:
|
|
60
|
-
r = c.post(url, json=payload, headers=_base_headers(self.token, self.tenant_id))
|
|
61
|
-
r.raise_for_status()
|
|
62
|
-
return r.json()
|
|
63
|
-
|
|
64
|
-
|
|
@@ -3,7 +3,6 @@ FabriqClient: High-level async helper for Agentic Fabric Gateway.
|
|
|
3
3
|
|
|
4
4
|
This client wraps common Gateway API flows so agent developers can:
|
|
5
5
|
- List and invoke tools
|
|
6
|
-
- Register and invoke MCP servers (via proxy)
|
|
7
6
|
- Invoke agents (via proxy)
|
|
8
7
|
- Manage per-user secrets via the Gateway-backed Vault API
|
|
9
8
|
|
|
@@ -120,43 +119,6 @@ class FabriqClient:
|
|
|
120
119
|
)
|
|
121
120
|
return r.json()
|
|
122
121
|
|
|
123
|
-
# -----------------
|
|
124
|
-
# MCP Servers
|
|
125
|
-
# -----------------
|
|
126
|
-
async def register_mcp_server(
|
|
127
|
-
self,
|
|
128
|
-
*,
|
|
129
|
-
name: str,
|
|
130
|
-
base_url: str,
|
|
131
|
-
description: Optional[str] = None,
|
|
132
|
-
auth_type: str = "API_KEY",
|
|
133
|
-
source: str = "STATIC",
|
|
134
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
135
|
-
) -> Dict[str, Any]:
|
|
136
|
-
body: Dict[str, Any] = {
|
|
137
|
-
"name": name,
|
|
138
|
-
"base_url": base_url,
|
|
139
|
-
"auth_type": auth_type,
|
|
140
|
-
"source": source,
|
|
141
|
-
}
|
|
142
|
-
if description is not None:
|
|
143
|
-
body["description"] = description
|
|
144
|
-
if metadata is not None:
|
|
145
|
-
body["metadata"] = metadata
|
|
146
|
-
r = await self._http.post("/mcp/servers", json=body, headers=self._extra_headers)
|
|
147
|
-
return r.json()
|
|
148
|
-
|
|
149
|
-
async def list_mcp_servers(self, *, page: int = 1, page_size: int = 20, search: Optional[str] = None) -> Dict[str, Any]:
|
|
150
|
-
params: Dict[str, Any] = {"page": page, "page_size": page_size}
|
|
151
|
-
if search:
|
|
152
|
-
params["search"] = search
|
|
153
|
-
r = await self._http.get("/mcp/servers", params=params, headers=self._extra_headers)
|
|
154
|
-
return r.json()
|
|
155
|
-
|
|
156
|
-
async def invoke_mcp(self, *, server_id: str, payload: Dict[str, Any], raw: bool = False) -> Dict[str, Any]:
|
|
157
|
-
r = await self._http.post(f"/proxy/mcp/{server_id}/invoke", json={"payload": payload, "raw": raw}, headers=self._extra_headers)
|
|
158
|
-
return r.json()
|
|
159
|
-
|
|
160
122
|
# -----------------
|
|
161
123
|
# Secrets (Gateway-backed Vault)
|
|
162
124
|
# -----------------
|
|
@@ -5,8 +5,6 @@ Data models for Agentic Fabric SDK.
|
|
|
5
5
|
from .types import (
|
|
6
6
|
ErrorResponse,
|
|
7
7
|
HealthResponse,
|
|
8
|
-
McpServer,
|
|
9
|
-
McpServerCreate,
|
|
10
8
|
MetricsResponse,
|
|
11
9
|
OAuthToken,
|
|
12
10
|
PaginatedResponse,
|
|
@@ -24,8 +22,6 @@ __all__ = [
|
|
|
24
22
|
"Tool",
|
|
25
23
|
"ToolInvokeRequest",
|
|
26
24
|
"ToolInvokeResult",
|
|
27
|
-
"McpServer",
|
|
28
|
-
"McpServerCreate",
|
|
29
25
|
"Secret",
|
|
30
26
|
"SecretMetadata",
|
|
31
27
|
"SecretPutRequest",
|
|
@@ -75,28 +75,6 @@ class ToolInvokeResult(BaseModel):
|
|
|
75
75
|
headers: Dict[str, str] = Field(default_factory=dict)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
class McpServer(BaseModel):
|
|
79
|
-
"""MCP Server model."""
|
|
80
|
-
|
|
81
|
-
id: UUID
|
|
82
|
-
base_url: str
|
|
83
|
-
auth_type: str = Field(description="One of: API_KEY, MTLS, OIDC")
|
|
84
|
-
source: str = Field(description="One of: STATIC, DISCOVERED, SLACK, CONFIG")
|
|
85
|
-
tenant_id: str
|
|
86
|
-
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
87
|
-
created_at: datetime
|
|
88
|
-
updated_at: datetime
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class McpServerCreate(BaseModel):
|
|
92
|
-
"""Request model for creating an MCP server."""
|
|
93
|
-
|
|
94
|
-
base_url: str
|
|
95
|
-
auth_type: str = Field(description="One of: API_KEY, MTLS, OIDC")
|
|
96
|
-
source: str = Field(description="One of: STATIC, DISCOVERED, SLACK, CONFIG")
|
|
97
|
-
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
98
|
-
|
|
99
|
-
|
|
100
78
|
class Secret(BaseModel):
|
|
101
79
|
"""Secret model."""
|
|
102
80
|
|
|
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
5
5
|
|
|
6
6
|
[tool.poetry]
|
|
7
7
|
name = "agentic-fabriq-sdk"
|
|
8
|
-
version = "0.1.
|
|
8
|
+
version = "0.1.25"
|
|
9
9
|
description = "Agentic Fabriq SDK: high-level client, CLI tool, DX helpers, and auth for AI agents"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = "Apache-2.0"
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MCP server management commands for the Agentic Fabric CLI.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import typer
|
|
6
|
-
|
|
7
|
-
from af_cli.core.client import get_client
|
|
8
|
-
from af_cli.core.output import error, info, print_output, success, warning
|
|
9
|
-
|
|
10
|
-
app = typer.Typer(help="MCP server management commands")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@app.command()
|
|
14
|
-
def list(
|
|
15
|
-
format: str = typer.Option("table", "--format", help="Output format"),
|
|
16
|
-
):
|
|
17
|
-
"""List MCP servers."""
|
|
18
|
-
try:
|
|
19
|
-
with get_client() as client:
|
|
20
|
-
response = client.get("/api/v1/mcp-servers")
|
|
21
|
-
servers = response["servers"]
|
|
22
|
-
|
|
23
|
-
if not servers:
|
|
24
|
-
warning("No MCP servers found")
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
print_output(
|
|
28
|
-
servers,
|
|
29
|
-
format_type=format,
|
|
30
|
-
title="MCP Servers"
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
except Exception as e:
|
|
34
|
-
error(f"Failed to list MCP servers: {e}")
|
|
35
|
-
raise typer.Exit(1)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@app.command()
|
|
39
|
-
def get(
|
|
40
|
-
server_id: str = typer.Argument(..., help="MCP server ID"),
|
|
41
|
-
format: str = typer.Option("table", "--format", help="Output format"),
|
|
42
|
-
):
|
|
43
|
-
"""Get MCP server details."""
|
|
44
|
-
try:
|
|
45
|
-
with get_client() as client:
|
|
46
|
-
server = client.get(f"/api/v1/mcp-servers/{server_id}")
|
|
47
|
-
|
|
48
|
-
print_output(
|
|
49
|
-
server,
|
|
50
|
-
format_type=format,
|
|
51
|
-
title=f"MCP Server {server_id}"
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
except Exception as e:
|
|
55
|
-
error(f"Failed to get MCP server: {e}")
|
|
56
|
-
raise typer.Exit(1)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
@app.command()
|
|
60
|
-
def create(
|
|
61
|
-
name: str = typer.Option(..., "--name", help="MCP server name"),
|
|
62
|
-
base_url: str = typer.Option(..., "--base-url", help="Base URL"),
|
|
63
|
-
auth_type: str = typer.Option("API_KEY", "--auth-type", help="Authentication type"),
|
|
64
|
-
):
|
|
65
|
-
"""Create a new MCP server."""
|
|
66
|
-
try:
|
|
67
|
-
with get_client() as client:
|
|
68
|
-
data = {
|
|
69
|
-
"name": name,
|
|
70
|
-
"base_url": base_url,
|
|
71
|
-
"auth_type": auth_type,
|
|
72
|
-
"source": "STATIC",
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
server = client.post("/api/v1/mcp-servers", data)
|
|
76
|
-
|
|
77
|
-
success(f"MCP server created: {server['id']}")
|
|
78
|
-
info(f"Name: {server['name']}")
|
|
79
|
-
info(f"Base URL: {server['base_url']}")
|
|
80
|
-
|
|
81
|
-
except Exception as e:
|
|
82
|
-
error(f"Failed to create MCP server: {e}")
|
|
83
|
-
raise typer.Exit(1)
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Secret management commands for the Agentic Fabric CLI.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import typer
|
|
6
|
-
|
|
7
|
-
from af_cli.core.client import get_client
|
|
8
|
-
from af_cli.core.output import error, info, print_output, success
|
|
9
|
-
|
|
10
|
-
app = typer.Typer(help="Secret management commands")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@app.command()
|
|
14
|
-
def get(
|
|
15
|
-
path: str = typer.Argument(..., help="Secret path"),
|
|
16
|
-
format: str = typer.Option("table", "--format", help="Output format"),
|
|
17
|
-
):
|
|
18
|
-
"""Get a secret."""
|
|
19
|
-
try:
|
|
20
|
-
with get_client() as client:
|
|
21
|
-
secret = client.get(f"/api/v1/secrets/{path}")
|
|
22
|
-
|
|
23
|
-
# Don't display the actual secret value in table format
|
|
24
|
-
if format == "table":
|
|
25
|
-
display_data = {
|
|
26
|
-
"path": secret["path"],
|
|
27
|
-
"description": secret.get("description", ""),
|
|
28
|
-
"version": secret["version"],
|
|
29
|
-
"created_at": secret["created_at"],
|
|
30
|
-
"updated_at": secret["updated_at"],
|
|
31
|
-
}
|
|
32
|
-
print_output(display_data, format_type=format, title=f"Secret {path}")
|
|
33
|
-
info("Use --format=json to see the secret value")
|
|
34
|
-
else:
|
|
35
|
-
print_output(secret, format_type=format)
|
|
36
|
-
|
|
37
|
-
except Exception as e:
|
|
38
|
-
error(f"Failed to get secret: {e}")
|
|
39
|
-
raise typer.Exit(1)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@app.command()
|
|
43
|
-
def create(
|
|
44
|
-
path: str = typer.Argument(..., help="Secret path"),
|
|
45
|
-
value: str = typer.Option(..., "--value", help="Secret value"),
|
|
46
|
-
description: str = typer.Option("", "--description", help="Secret description"),
|
|
47
|
-
):
|
|
48
|
-
"""Create a new secret."""
|
|
49
|
-
try:
|
|
50
|
-
with get_client() as client:
|
|
51
|
-
data = {
|
|
52
|
-
"value": value,
|
|
53
|
-
"description": description,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
secret = client.post(f"/api/v1/secrets/{path}", data)
|
|
57
|
-
|
|
58
|
-
success(f"Secret created: {secret['path']}")
|
|
59
|
-
info(f"Version: {secret['version']}")
|
|
60
|
-
|
|
61
|
-
except Exception as e:
|
|
62
|
-
error(f"Failed to create secret: {e}")
|
|
63
|
-
raise typer.Exit(1)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
@app.command()
|
|
67
|
-
def update(
|
|
68
|
-
path: str = typer.Argument(..., help="Secret path"),
|
|
69
|
-
value: str = typer.Option(..., "--value", help="Secret value"),
|
|
70
|
-
description: str = typer.Option("", "--description", help="Secret description"),
|
|
71
|
-
):
|
|
72
|
-
"""Update a secret."""
|
|
73
|
-
try:
|
|
74
|
-
with get_client() as client:
|
|
75
|
-
data = {
|
|
76
|
-
"value": value,
|
|
77
|
-
"description": description,
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
secret = client.put(f"/api/v1/secrets/{path}", data)
|
|
81
|
-
|
|
82
|
-
success(f"Secret updated: {secret['path']}")
|
|
83
|
-
info(f"Version: {secret['version']}")
|
|
84
|
-
|
|
85
|
-
except Exception as e:
|
|
86
|
-
error(f"Failed to update secret: {e}")
|
|
87
|
-
raise typer.Exit(1)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@app.command()
|
|
91
|
-
def delete(
|
|
92
|
-
path: str = typer.Argument(..., help="Secret path"),
|
|
93
|
-
force: bool = typer.Option(False, "--force", help="Force deletion"),
|
|
94
|
-
):
|
|
95
|
-
"""Delete a secret."""
|
|
96
|
-
try:
|
|
97
|
-
if not force:
|
|
98
|
-
if not typer.confirm(f"Are you sure you want to delete secret {path}?"):
|
|
99
|
-
info("Deletion cancelled")
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
with get_client() as client:
|
|
103
|
-
client.delete(f"/api/v1/secrets/{path}")
|
|
104
|
-
|
|
105
|
-
success(f"Secret deleted: {path}")
|
|
106
|
-
|
|
107
|
-
except Exception as e:
|
|
108
|
-
error(f"Failed to delete secret: {e}")
|
|
109
|
-
raise typer.Exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|