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.

Files changed (41) hide show
  1. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/PKG-INFO +22 -3
  2. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/README.md +21 -2
  3. agentic_fabriq_sdk-0.1.25/af_cli/__init__.py +17 -0
  4. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/applications.py +121 -12
  5. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/auth.py +8 -7
  6. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/config.py +37 -11
  7. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/tools.py +147 -15
  8. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/client.py +6 -1
  9. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/config.py +2 -1
  10. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/main.py +6 -10
  11. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/oauth.py +3 -0
  12. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/connectors/__init__.py +0 -2
  13. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/connectors/base.py +9 -42
  14. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/dx/__init__.py +1 -2
  15. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/dx/runtime.py +0 -22
  16. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/fabriq_client.py +0 -38
  17. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/models/__init__.py +0 -4
  18. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/models/types.py +0 -22
  19. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/pyproject.toml +1 -1
  20. agentic_fabriq_sdk-0.1.23/af_cli/__init__.py +0 -8
  21. agentic_fabriq_sdk-0.1.23/af_cli/commands/mcp_servers.py +0 -83
  22. agentic_fabriq_sdk-0.1.23/af_cli/commands/secrets.py +0 -109
  23. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/commands/__init__.py +0 -0
  24. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/__init__.py +0 -0
  25. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/oauth.py +0 -0
  26. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/output.py +0 -0
  27. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_cli/core/token_storage.py +0 -0
  28. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/__init__.py +0 -0
  29. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/__init__.py +0 -0
  30. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/applications.py +0 -0
  31. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/dpop.py +0 -0
  32. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/auth/token_cache.py +0 -0
  33. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/connectors/registry.py +0 -0
  34. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/dx/decorators.py +0 -0
  35. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/events.py +0 -0
  36. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/exceptions.py +0 -0
  37. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/models/audit.py +0 -0
  38. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/py.typed +0 -0
  39. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/transport/__init__.py +0 -0
  40. {agentic_fabriq_sdk-0.1.23 → agentic_fabriq_sdk-0.1.25}/af_sdk/transport/http.py +0 -0
  41. {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.23
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 using connection ID
91
+ # Invoke Slack tools
92
92
  result = await af.invoke_connection("my-slack", method="get_channels")
93
93
 
94
- # Or post a message
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 using connection ID
55
+ # Invoke Slack tools
56
56
  result = await af.invoke_connection("my-slack", method="get_channels")
57
57
 
58
- # Or post a message
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("create")
22
- def create_application(
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 create \\
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 registered successfully!", style="green bold")
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 both:
124
- - Local config files (~/.af/applications/)
125
- - Server (via API)
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 locally.", style="yellow")
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
- # Use direct localhost redirect until branded page is deployed
93
- tokens = oauth_client.login(open_browser=browser, timeout=300, use_hosted_callback=False)
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("table", "--format", help="Output format"),
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
- "env_gateway_url": os.getenv("AF_GATEWAY_URL", "Not set"),
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=format,
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
- "tenant_id": "tenant_id",
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
- value = getattr(config, valid_keys[key])
84
- info(f"{key}: {value}")
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
- connections = client.get("/api/v1/user-connections")
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
- warning("No tool connections found. Add connections in the dashboard UI.")
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 with your Google account")
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
- confirm = typer.confirm("Do you want to reconnect (re-authorize)?")
400
- if not confirm:
401
- return
402
-
403
- # Determine the API base tool name (Google tools all use "google")
404
- api_tool_name = "google" if (tool.startswith("google_") or tool == "gmail") else tool
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 != api_tool_name else ""
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
- debug(f"GET {url}")
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
- # Gateway settings (support environment variable override)
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
- force: bool = typer.Option(
91
+ yes: bool = typer.Option(
96
92
  False,
97
- "--force",
98
- help="Force initialization, overwrite existing config"
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 force:
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 --force to overwrite")
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 agents, tools, MCP servers, and secrets
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 or MCP."""
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:
@@ -1,10 +1,9 @@
1
1
  from .decorators import tool
2
- from .runtime import ToolFabric, MCPServer
2
+ from .runtime import ToolFabric
3
3
 
4
4
  __all__ = [
5
5
  "tool",
6
6
  "ToolFabric",
7
- "MCPServer",
8
7
  ]
9
8
 
10
9
 
@@ -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.23"
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,8 +0,0 @@
1
- """
2
- Agentic Fabric CLI Tool
3
-
4
- A command-line interface for managing Agentic Fabric resources including
5
- agents, tools, MCP servers, and administrative operations.
6
- """
7
-
8
- __version__ = "1.0.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)