diff --git a/AGENTS.md b/AGENTS.md index 3a77075..e67a23c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,14 +28,13 @@ func (c *MyCmd) Run(ctx *commands.Context) error { if ctx.DryRun { return ctx.ValidateDryRun(endpoint(), "ToolName", "do X", displayData, mcpArgs) } // 2. Confirm guard (destructive ops only) if err := ctx.Confirm("delete X"); err != nil { return err } - // 3. Create client + initialize - client := ctx.NewMCPClient(endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { ... } - // 4. Call MCP tool - resp, err := client.CallTool(ctx.Ctx, "ToolName", map[string]any{...}) - // 5. Extract + print - data, err := output.ExtractContent(resp) - return ctx.Output.PrintList("items", output.SomeColumns, rows) // or PrintItem/PrintMutation + // 3. Build MCP args + args := map[string]any{...} + // 4. Call MCP tool + extract content via the shared execution seam + data, err := ctx.CallToolData(endpoint(), "ToolName", "do X", args) + if err != nil { return err } + // 5. Print + return ctx.Output.PrintListFromData("items", output.SomeColumns, data, 0, "items", "value") // or PrintItem/PrintMutation } ``` @@ -44,7 +43,8 @@ Adding a new service = new directory in `internal/commands/`, register in `main. ## Key Conventions - Use `config.Endpoint("service")` for server URLs, never hardcode -- Use `output.ExtractContent()` then `ToRows()` for list data +- Use `ctx.CallToolData()` for the normal initialize/call/extract path; reserve `ctx.NewMCPClient()` for custom protocol flows such as `tools/list` +- Use `ctx.Output.PrintListFromData()` for standard list extraction/fallback/limit handling - Write ops: always add `--dry-run` guard with `ctx.ValidateDryRun(endpoint, toolName, action, displayData, mcpArgs)` - When display keys differ from MCP arg keys, pass mcpArgs as the 5th parameter for correct validation - Destructive ops: add `ctx.Confirm()` after dry-run check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 114ac9f..d0220c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,28 +83,11 @@ type MyServiceListCmd struct { } func (c *MyServiceListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ListItems", map[string]any{}) - if err != nil { - return fmt.Errorf("list items: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(endpoint(), "ListItems", "list items", map[string]any{}) if err != nil { return err } - rows := output.ToRows(data, "items") - if rows == nil { - rows = output.ToRows(data, "value") - } - if rows == nil { - return ctx.Output.PrintItem(data) - } - return ctx.Output.PrintList("items", output.MyColumns, rows) + return ctx.Output.PrintListFromData("items", output.MyColumns, data, c.Max, "items", "value") } ``` @@ -148,7 +131,7 @@ go test ./internal/mcp/... -v # MCP client tests only go test ./internal/output/... -v # Output formatting tests only ``` -The test suite uses `httptest.NewServer` for MCP client tests and `bytes.Buffer` injection for output formatter tests. Use `testutil.SetupTestServerWithSchemas` for dry-run tests that verify schema validation. No real network calls in tests. +The test suite uses `httptest.NewServer` for MCP client tests, `commands.Context.CallToolData` tests for command execution, and `bytes.Buffer` injection for output formatter tests. Use `testutil.SetupTestServerWithSchemas` for dry-run tests that verify schema validation. No real network calls in tests. ## Discovering MCP Tools diff --git a/docs/architecture.md b/docs/architecture.md index 1395c8b..b4557b8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -32,8 +32,9 @@ Every command follows the same path: 5. **Request** — `CallTool()` sends a JSON-RPC `tools/call` POST with `Authorization: Bearer` and `Mcp-Session-Id` headers 6. **Retry** — on 502/503/429/504, retries up to 2x with exponential backoff (1s, 2s); respects `Retry-After` header 7. **Response** — parses SSE stream (`data:` lines) or plain JSON; extracts the first JSON-RPC message -8. **Extract** — `ExtractContent()` unwraps 3 response patterns (clean JSON, embedded JSON after status text, rawResponse-wrapped) -9. **Render** — `PrintList`/`PrintItem`/`PrintMutation` dispatches to table (tabwriter), JSON, or TSV based on `--output` +8. **Command execution seam** — command handlers use `Context.CallToolData()` to keep initialize → tool call → content extraction and action-scoped errors in one module +9. **Extract** — `ExtractContent()` unwraps 3 response patterns (clean JSON, embedded JSON after status text, rawResponse-wrapped) +10. **Render** — `PrintListFromData`/`PrintList`/`PrintItem`/`PrintMutation` dispatches to table (tabwriter), JSON, or TSV based on `--output` ## MCP Protocol @@ -95,10 +96,11 @@ Browser ──PKCE──► Entra ID (login.microsoftonline.com) ## Output Pipeline ``` -MCP JSONRPCResponse - → ExtractContent() (extract.go) → map[string]any - → ToRows() (extract.go) → []map[string]any - → PrintList() (formatter.go) → format dispatch +Command handler + → Context.CallToolData() (commands/root.go) → MCP JSONRPCResponse + → ExtractContent() (extract.go) → map[string]any + → PrintListFromData() (formatter.go) → ToRows() + fallback + max + → PrintList() (formatter.go) → format dispatch ├── FormatHuman → RenderTable() (render.go) → text/tabwriter ├── FormatJSON → writeJSON() (formatter.go) → json.Encoder └── FormatPlain → RenderTSV() (render.go) → raw tabs diff --git a/docs/excel.md b/docs/excel.md index 2d9dfab..7e79c0f 100644 --- a/docs/excel.md +++ b/docs/excel.md @@ -6,16 +6,18 @@ Create, read, and collaborate on Microsoft Excel workbooks. Supports creating ne | Command | Description | Key Arguments | |---------|-------------|---------------| -| `excel create` | Create a new Excel workbook | `` | -| `excel get` | Get workbook content | ` ` | +| `excel create` | Create a new Excel workbook | `` `--csv-content` | +| `excel get` | Get workbook content | `` | | `excel comment` | Add a comment to a workbook cell | ` ` | | `excel reply` | Reply to a workbook comment | ` ` | ## Arguments - **``** -- Desired file name for the new workbook. -- **``** -- OneDrive or SharePoint drive ID. -- **``** -- Document ID within the drive. +- **`--csv-content`** -- CSV content used to populate a newly created workbook (defaults to empty). +- **``** -- SharePoint sharing URL for the workbook. +- **``** -- OneDrive or SharePoint drive ID (used by comment/reply). +- **``** -- Document ID within the drive (used by comment/reply). - **``** -- Cell address for the comment (e.g. `A1`, `B2`, `C10`). - **``** -- ID of the comment to reply to. - **``** -- Comment or reply text. @@ -25,13 +27,13 @@ Create, read, and collaborate on Microsoft Excel workbooks. Supports creating ne ```sh # Create a new workbook -a365 excel create "Q3 Budget.xlsx" +a365 excel create "Q3 Budget.xlsx" --csv-content $'category,amount\ntravel,100' # Preview creation without making changes a365 excel create "Expenses.xlsx" --dry-run # Get workbook content -a365 excel get b!xYzDriveId01 01ABCDEF23456789 +a365 excel get "https://contoso.sharepoint.com/sites/finance/Shared%20Documents/Budget.xlsx" # Add a comment at cell B5 a365 excel comment b!xYzDriveId01 01ABCDEF23456789 B5 "This value looks off" @@ -43,5 +45,5 @@ a365 excel comment b!xYzDriveId01 01ABCDEF23456789 A1 "Check formula" --dry-run a365 excel reply comment-id-789 b!xYzDriveId01 01ABCDEF23456789 "Fixed the formula" # Output as JSON -a365 excel get b!xYzDriveId01 01ABCDEF23456789 --output json +a365 excel get "https://contoso.sharepoint.com/sites/finance/Shared%20Documents/Budget.xlsx" --output json ``` diff --git a/docs/word.md b/docs/word.md index 31ce46d..a179e36 100644 --- a/docs/word.md +++ b/docs/word.md @@ -6,16 +6,18 @@ Create, read, and collaborate on Microsoft Word documents. Supports creating new | Command | Description | Key Arguments | |---------|-------------|---------------| -| `word create` | Create a new Word document | `` | -| `word get` | Get document content | ` ` | +| `word create` | Create a new Word document | `` `--content` | +| `word get` | Get document content | `` | | `word comment` | Add a comment to a document | ` ` | | `word reply` | Reply to a document comment | ` ` | ## Arguments - **``** -- Desired file name for the new document. -- **``** -- OneDrive or SharePoint drive ID. -- **``** -- Document ID within the drive. +- **`--content`** -- HTML or plain text content for the document body (defaults to empty). +- **``** -- SharePoint sharing URL for the document. +- **``** -- OneDrive or SharePoint drive ID (used by comment/reply). +- **``** -- Document ID within the drive (used by comment/reply). - **``** -- ID of the comment to reply to. - **``** -- Comment or reply text. - **`--dry-run`** -- Preview write operations without executing them (supported by `create`, `comment`, and `reply`). @@ -24,13 +26,13 @@ Create, read, and collaborate on Microsoft Word documents. Supports creating new ```sh # Create a new document -a365 word create "Project Proposal.docx" +a365 word create "Project Proposal.docx" --content "

Project Proposal

" # Preview creation without making changes a365 word create "Draft Notes.docx" --dry-run # Get document content -a365 word get b!xYzDriveId01 01ABCDEF23456789 +a365 word get "https://contoso.sharepoint.com/sites/project/Shared%20Documents/Proposal.docx" # Add a comment to a document a365 word comment b!xYzDriveId01 01ABCDEF23456789 "Please review section 3" @@ -42,5 +44,5 @@ a365 word reply comment-id-456 b!xYzDriveId01 01ABCDEF23456789 "Done, updated." a365 word reply comment-id-456 b!xYzDriveId01 01ABCDEF23456789 "Looks good" --dry-run # Output as JSON -a365 word get b!xYzDriveId01 01ABCDEF23456789 --output json +a365 word get "https://contoso.sharepoint.com/sites/project/Shared%20Documents/Proposal.docx" --output json ``` diff --git a/internal/commands/admin/admin.go b/internal/commands/admin/admin.go index af310be..ff03fa2 100644 --- a/internal/commands/admin/admin.go +++ b/internal/commands/admin/admin.go @@ -5,7 +5,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // AdminCmd groups M365 admin subcommands. @@ -25,20 +24,10 @@ type AdminSearchUsersCmd struct { } func (c *AdminSearchUsersCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(adminEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "mcp_Admin365_SearchUserTools", map[string]any{ + data, err := ctx.CallToolData(adminEndpoint(), "mcp_Admin365_SearchUserTools", "search users", map[string]any{ "searchTerm": c.Query, "ConsistencyLevel": "eventual", }) - if err != nil { - return fmt.Errorf("search users: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -49,17 +38,7 @@ func (c *AdminSearchUsersCmd) Run(ctx *commands.Context) error { type AdminListLicensesCmd struct{} func (c *AdminListLicensesCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(adminEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "mcp_Admin365_ListLicenseTools", map[string]any{}) - if err != nil { - return fmt.Errorf("list licenses: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(adminEndpoint(), "mcp_Admin365_ListLicenseTools", "list licenses", map[string]any{}) if err != nil { return err } @@ -86,26 +65,16 @@ func (c *AdminSetLicenseCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(adminEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - addList := make([]map[string]any, 0, len(c.AddLicenses)) for _, sku := range c.AddLicenses { addList = append(addList, map[string]any{"skuId": sku}) } - resp, err := client.CallTool(ctx.Ctx, "mcp_Admin365_LicenseMgmtTools", map[string]any{ + data, err := ctx.CallToolData(adminEndpoint(), "mcp_Admin365_LicenseMgmtTools", "set license", map[string]any{ "userId": c.UserID, "addLicenses": addList, "removeLicenses": c.RemoveLicenses, }) - if err != nil { - return fmt.Errorf("set license: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/admin365/admin365.go b/internal/commands/admin365/admin365.go index 8ce63d5..695d7ef 100644 --- a/internal/commands/admin365/admin365.go +++ b/internal/commands/admin365/admin365.go @@ -5,7 +5,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // admin365Endpoint returns the agent365 endpoint for the Admin365 MCP server. @@ -15,7 +14,7 @@ func admin365Endpoint() string { // Admin365Cmd groups all Admin365 subcommands. type Admin365Cmd struct { - BulkAdd Admin365BulkAddCmd `cmd:"" name:"bulk-add" help:"Bulk add users to tenant"` + BulkAdd Admin365BulkAddCmd `cmd:"" name:"bulk-add" help:"Bulk add users to tenant"` AgentAccess Admin365AgentAccessCmd `cmd:"" name:"agent-access" help:"Get agent access settings"` AgentSharing Admin365AgentSharingCmd `cmd:"" name:"agent-sharing" help:"Get agent sharing settings"` MsApps Admin365MsAppsCmd `cmd:"" name:"ms-apps" help:"Get Microsoft apps install settings"` @@ -49,19 +48,9 @@ func (c *Admin365BulkAddCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "BulkAddUsers", map[string]any{ + data, err := ctx.CallToolData(admin365Endpoint(), "BulkAddUsers", "bulk add users", map[string]any{ "fileContent": c.FileContent, }) - if err != nil { - return fmt.Errorf("bulk add users: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -74,17 +63,7 @@ func (c *Admin365BulkAddCmd) Run(ctx *commands.Context) error { type Admin365AgentAccessCmd struct{} func (c *Admin365AgentAccessCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetWhoCanAccessAgentsSettings", map[string]any{}) - if err != nil { - return fmt.Errorf("get agent access settings: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(admin365Endpoint(), "GetWhoCanAccessAgentsSettings", "get agent access settings", map[string]any{}) if err != nil { return err } @@ -97,17 +76,7 @@ func (c *Admin365AgentAccessCmd) Run(ctx *commands.Context) error { type Admin365AgentSharingCmd struct{} func (c *Admin365AgentSharingCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetWhoCanShareAgentsOrgWideSettings", map[string]any{}) - if err != nil { - return fmt.Errorf("get agent sharing settings: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(admin365Endpoint(), "GetWhoCanShareAgentsOrgWideSettings", "get agent sharing settings", map[string]any{}) if err != nil { return err } @@ -120,17 +89,7 @@ func (c *Admin365AgentSharingCmd) Run(ctx *commands.Context) error { type Admin365MsAppsCmd struct{} func (c *Admin365MsAppsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetCanInstallMicrosoftAppsAndAgentsSettings", map[string]any{}) - if err != nil { - return fmt.Errorf("get Microsoft apps settings: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(admin365Endpoint(), "GetCanInstallMicrosoftAppsAndAgentsSettings", "get Microsoft apps settings", map[string]any{}) if err != nil { return err } @@ -143,17 +102,7 @@ func (c *Admin365MsAppsCmd) Run(ctx *commands.Context) error { type Admin365ThirdPartyCmd struct{} func (c *Admin365ThirdPartyCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetCanInstallThirdPartyAppsAndAgentsSettings", map[string]any{}) - if err != nil { - return fmt.Errorf("get third-party apps settings: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(admin365Endpoint(), "GetCanInstallThirdPartyAppsAndAgentsSettings", "get third-party apps settings", map[string]any{}) if err != nil { return err } @@ -166,17 +115,7 @@ func (c *Admin365ThirdPartyCmd) Run(ctx *commands.Context) error { type Admin365LobAppsCmd struct{} func (c *Admin365LobAppsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetCanInstallLOBAppsAndAgentsSettings", map[string]any{}) - if err != nil { - return fmt.Errorf("get LOB apps settings: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(admin365Endpoint(), "GetCanInstallLOBAppsAndAgentsSettings", "get LOB apps settings", map[string]any{}) if err != nil { return err } @@ -201,19 +140,9 @@ func (c *Admin365SetAccessCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateWhoCanAccessAgentsSettings", map[string]any{ + data, err := ctx.CallToolData(admin365Endpoint(), "UpdateWhoCanAccessAgentsSettings", "update agent access", map[string]any{ "accessLevel": c.AccessLevel, }) - if err != nil { - return fmt.Errorf("update agent access: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -238,19 +167,9 @@ func (c *Admin365SetSharingCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateWhoCanShareAgentsOrgWideSettings", map[string]any{ + data, err := ctx.CallToolData(admin365Endpoint(), "UpdateWhoCanShareAgentsOrgWideSettings", "update agent sharing", map[string]any{ "accessLevel": c.AccessLevel, }) - if err != nil { - return fmt.Errorf("update agent sharing: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -275,19 +194,9 @@ func (c *Admin365SetMsAppsCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateCanInstallMicrosoftAppsAndAgentsSettings", map[string]any{ + data, err := ctx.CallToolData(admin365Endpoint(), "UpdateCanInstallMicrosoftAppsAndAgentsSettings", "update Microsoft apps settings", map[string]any{ "allowed": c.Allowed, }) - if err != nil { - return fmt.Errorf("update Microsoft apps settings: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -312,19 +221,9 @@ func (c *Admin365SetThirdPartyCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateCanInstallThirdPartyAppsAndAgentsSettings", map[string]any{ + data, err := ctx.CallToolData(admin365Endpoint(), "UpdateCanInstallThirdPartyAppsAndAgentsSettings", "update third-party apps settings", map[string]any{ "allowed": c.Allowed, }) - if err != nil { - return fmt.Errorf("update third-party apps settings: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -349,19 +248,9 @@ func (c *Admin365SetLobAppsCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateCanInstallLOBAppsAndAgentsSettings", map[string]any{ + data, err := ctx.CallToolData(admin365Endpoint(), "UpdateCanInstallLOBAppsAndAgentsSettings", "update LOB apps settings", map[string]any{ "allowed": c.Allowed, }) - if err != nil { - return fmt.Errorf("update LOB apps settings: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -374,17 +263,7 @@ func (c *Admin365SetLobAppsCmd) Run(ctx *commands.Context) error { type Admin365ReadinessCmd struct{} func (c *Admin365ReadinessCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetCopilotReadiness", map[string]any{}) - if err != nil { - return fmt.Errorf("get Copilot readiness: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(admin365Endpoint(), "GetCopilotReadiness", "get Copilot readiness", map[string]any{}) if err != nil { return err } @@ -397,17 +276,7 @@ func (c *Admin365ReadinessCmd) Run(ctx *commands.Context) error { type Admin365CopilotStatusCmd struct{} func (c *Admin365CopilotStatusCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetCopilotAdminSettings", map[string]any{}) - if err != nil { - return fmt.Errorf("get Copilot admin settings: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(admin365Endpoint(), "GetCopilotAdminSettings", "get Copilot admin settings", map[string]any{}) if err != nil { return err } @@ -432,19 +301,9 @@ func (c *Admin365SetCopilotCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(admin365Endpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateCopilotAdminSettings", map[string]any{ + data, err := ctx.CallToolData(admin365Endpoint(), "UpdateCopilotAdminSettings", "update Copilot admin settings", map[string]any{ "isEnabled": c.IsEnabled, }) - if err != nil { - return fmt.Errorf("update Copilot admin settings: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/api/api.go b/internal/commands/api/api.go index 9d3b03a..2ce6066 100644 --- a/internal/commands/api/api.go +++ b/internal/commands/api/api.go @@ -144,17 +144,7 @@ func (c *APICallCmd) Run(ctx *commands.Context) error { return fmt.Errorf("invalid JSON arguments: %w", err) } - client := ctx.NewMCPClient(endpoint) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, c.Tool, args) - if err != nil { - return fmt.Errorf("call %s: %w", c.Tool, err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(endpoint, c.Tool, fmt.Sprintf("call %s", c.Tool), args) if err != nil { return err } diff --git a/internal/commands/calendar/calendar.go b/internal/commands/calendar/calendar.go index 551735f..e61a532 100644 --- a/internal/commands/calendar/calendar.go +++ b/internal/commands/calendar/calendar.go @@ -10,19 +10,19 @@ import ( // CalendarCmd groups all Calendar subcommands. type CalendarCmd struct { - List CalListCmd `cmd:"" help:"List upcoming events"` - View CalViewCmd `cmd:"" help:"List events in a date range"` - Create CalCreateCmd `cmd:"" help:"Create a calendar event"` - Update CalUpdateCmd `cmd:"" help:"Update a calendar event"` - Delete CalDeleteCmd `cmd:"" help:"Delete a calendar event"` - Accept CalAcceptCmd `cmd:"" help:"Accept a meeting invite"` - Tentative CalTentativeCmd `cmd:"" help:"Tentatively accept a meeting invite"` - Decline CalDeclineCmd `cmd:"" help:"Decline a meeting invite"` - Cancel CalCancelCmd `cmd:"" help:"Cancel a meeting you organized"` - Forward CalForwardCmd `cmd:"" help:"Forward a meeting invite"` - FreeBusy CalFreeBusyCmd `cmd:"" name:"free-busy" help:"Find available meeting times"` - TimeZone CalTimeZoneCmd `cmd:"" name:"timezone" help:"Get user date/time zone settings"` - Rooms CalRoomsCmd `cmd:"" help:"List available rooms"` + List CalListCmd `cmd:"" help:"List upcoming events"` + View CalViewCmd `cmd:"" help:"List events in a date range"` + Create CalCreateCmd `cmd:"" help:"Create a calendar event"` + Update CalUpdateCmd `cmd:"" help:"Update a calendar event"` + Delete CalDeleteCmd `cmd:"" help:"Delete a calendar event"` + Accept CalAcceptCmd `cmd:"" help:"Accept a meeting invite"` + Tentative CalTentativeCmd `cmd:"" help:"Tentatively accept a meeting invite"` + Decline CalDeclineCmd `cmd:"" help:"Decline a meeting invite"` + Cancel CalCancelCmd `cmd:"" help:"Cancel a meeting you organized"` + Forward CalForwardCmd `cmd:"" help:"Forward a meeting invite"` + FreeBusy CalFreeBusyCmd `cmd:"" name:"free-busy" help:"Find available meeting times"` + TimeZone CalTimeZoneCmd `cmd:"" name:"timezone" help:"Get user date/time zone settings"` + Rooms CalRoomsCmd `cmd:"" help:"List available rooms"` } func calEndpoint() string { @@ -35,31 +35,11 @@ type CalListCmd struct { } func (c *CalListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ListEvents", map[string]any{}) - if err != nil { - return fmt.Errorf("list events: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "ListEvents", "list events", map[string]any{}) if err != nil { return err } - rows := output.ToRows(data, "events") - if rows == nil { - rows = output.ToRows(data, "value") - } - if rows == nil { - return ctx.Output.PrintItem(data) - } - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("events", output.CalendarColumns, rows) + return ctx.Output.PrintListFromData("events", output.CalendarColumns, data, c.Max, "events", "value") } // CalViewCmd lists events in a date range. @@ -68,31 +48,11 @@ type CalViewCmd struct { } func (c *CalViewCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ListCalendarView", map[string]any{}) - if err != nil { - return fmt.Errorf("list calendar view: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "ListCalendarView", "list calendar view", map[string]any{}) if err != nil { return err } - rows := output.ToRows(data, "events") - if rows == nil { - rows = output.ToRows(data, "value") - } - if rows == nil { - return ctx.Output.PrintItem(data) - } - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("events", output.CalendarColumns, rows) + return ctx.Output.PrintListFromData("events", output.CalendarColumns, data, c.Max, "events", "value") } // CalCreateCmd creates a calendar event. @@ -129,11 +89,6 @@ func (c *CalCreateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "subject": c.Subject, "startDateTime": c.Start, @@ -147,12 +102,7 @@ func (c *CalCreateCmd) Run(ctx *commands.Context) error { args["isOnlineMeeting"] = true } - resp, err := client.CallTool(ctx.Ctx, "CreateEvent", args) - if err != nil { - return fmt.Errorf("create event: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "CreateEvent", "create event", args) if err != nil { return err } @@ -161,11 +111,11 @@ func (c *CalCreateCmd) Run(ctx *commands.Context) error { // CalUpdateCmd updates a calendar event. type CalUpdateCmd struct { - ID string `arg:"" help:"Event ID"` - Subject string `help:"New subject" optional:""` - Start string `help:"New start time" optional:""` - End string `help:"New end time" optional:""` - Body string `help:"New body" optional:""` + ID string `arg:"" help:"Event ID"` + Subject string `help:"New subject" optional:""` + Start string `help:"New start time" optional:""` + End string `help:"New end time" optional:""` + Body string `help:"New body" optional:""` } func (c *CalUpdateCmd) Run(ctx *commands.Context) error { @@ -176,11 +126,6 @@ func (c *CalUpdateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{"eventId": c.ID} if c.Subject != "" { args["subject"] = c.Subject @@ -195,12 +140,7 @@ func (c *CalUpdateCmd) Run(ctx *commands.Context) error { args["body"] = c.Body } - resp, err := client.CallTool(ctx.Ctx, "UpdateEvent", args) - if err != nil { - return fmt.Errorf("update event: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "UpdateEvent", "update event", args) if err != nil { return err } @@ -221,17 +161,7 @@ func (c *CalDeleteCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "DeleteEventById", map[string]any{"eventId": c.ID}) - if err != nil { - return fmt.Errorf("delete event: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "DeleteEventById", "delete event", map[string]any{"eventId": c.ID}) if err != nil { return err } @@ -249,15 +179,7 @@ func (c *CalAcceptCmd) Run(ctx *commands.Context) error { map[string]any{"action": "calendar.accept", "eventId": c.ID}) } - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "AcceptEvent", map[string]any{"eventId": c.ID}) - if err != nil { - return fmt.Errorf("accept: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "AcceptEvent", "accept", map[string]any{"eventId": c.ID}) if err != nil { return err } @@ -275,15 +197,7 @@ func (c *CalTentativeCmd) Run(ctx *commands.Context) error { map[string]any{"action": "calendar.tentative", "eventId": c.ID}) } - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "TentativelyAcceptEvent", map[string]any{"eventId": c.ID}) - if err != nil { - return fmt.Errorf("tentative accept: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "TentativelyAcceptEvent", "tentative accept", map[string]any{"eventId": c.ID}) if err != nil { return err } @@ -301,15 +215,7 @@ func (c *CalDeclineCmd) Run(ctx *commands.Context) error { map[string]any{"action": "calendar.decline", "eventId": c.ID}) } - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "DeclineEvent", map[string]any{"eventId": c.ID}) - if err != nil { - return fmt.Errorf("decline: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "DeclineEvent", "decline", map[string]any{"eventId": c.ID}) if err != nil { return err } @@ -330,15 +236,7 @@ func (c *CalCancelCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "CancelEvent", map[string]any{"eventId": c.ID}) - if err != nil { - return fmt.Errorf("cancel: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "CancelEvent", "cancel", map[string]any{"eventId": c.ID}) if err != nil { return err } @@ -364,21 +262,12 @@ func (c *CalForwardCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{"eventId": c.ID, "recipientEmails": c.Recipients} if c.Comment != "" { args["comment"] = c.Comment } - resp, err := client.CallTool(ctx.Ctx, "ForwardEvent", args) - if err != nil { - return fmt.Errorf("forward: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "ForwardEvent", "forward", args) if err != nil { return err } @@ -389,15 +278,7 @@ func (c *CalForwardCmd) Run(ctx *commands.Context) error { type CalFreeBusyCmd struct{} func (c *CalFreeBusyCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "FindMeetingTimes", map[string]any{}) - if err != nil { - return fmt.Errorf("find meeting times: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "FindMeetingTimes", "find meeting times", map[string]any{}) if err != nil { return err } @@ -408,15 +289,7 @@ func (c *CalFreeBusyCmd) Run(ctx *commands.Context) error { type CalTimeZoneCmd struct{} func (c *CalTimeZoneCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "GetUserDateAndTimeZoneSettings", map[string]any{}) - if err != nil { - return fmt.Errorf("get timezone: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "GetUserDateAndTimeZoneSettings", "get timezone", map[string]any{}) if err != nil { return err } @@ -427,15 +300,7 @@ func (c *CalTimeZoneCmd) Run(ctx *commands.Context) error { type CalRoomsCmd struct{} func (c *CalRoomsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(calEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "GetRooms", map[string]any{}) - if err != nil { - return fmt.Errorf("get rooms: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(calEndpoint(), "GetRooms", "get rooms", map[string]any{}) if err != nil { return err } diff --git a/internal/commands/copilot/agents.go b/internal/commands/copilot/agents.go index 4810175..72b53f1 100644 --- a/internal/commands/copilot/agents.go +++ b/internal/commands/copilot/agents.go @@ -42,17 +42,7 @@ func copilotAgentsEndpoint() string { } func fetchAvailableAgents(ctx *commands.Context) ([]agentInfo, error) { - client := ctx.NewMCPClient(copilotAgentsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return nil, fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, copilotAvailableAgentsTool, map[string]any{}) - if err != nil { - return nil, fmt.Errorf("list Copilot agents: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(copilotAgentsEndpoint(), copilotAvailableAgentsTool, "list Copilot agents", map[string]any{}) if err != nil { return nil, err } diff --git a/internal/commands/copilot/copilot.go b/internal/commands/copilot/copilot.go index 00d93b1..0f45d73 100644 --- a/internal/commands/copilot/copilot.go +++ b/internal/commands/copilot/copilot.go @@ -127,11 +127,6 @@ func callCopilot(ctx *commands.Context, message, conversationID, agentSelector s stopSpinner := startCopilotSpinner(ctx) defer stopSpinner() - client := ctx.NewMCPClient(copilotEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return nil, "", fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "enableWebSearch": enableWebSearch, "message": message, @@ -144,12 +139,7 @@ func callCopilot(ctx *commands.Context, message, conversationID, agentSelector s } for attempt := 0; ; attempt++ { - resp, err := client.CallTool(ctx.Ctx, copilotChatTool, args) - if err != nil { - return nil, "", fmt.Errorf("copilot chat: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(copilotEndpoint(), copilotChatTool, "copilot chat", args) if err != nil { return nil, "", err } diff --git a/internal/commands/dasearch/dasearch.go b/internal/commands/dasearch/dasearch.go index 6ae6dbb..b1a3a45 100644 --- a/internal/commands/dasearch/dasearch.go +++ b/internal/commands/dasearch/dasearch.go @@ -1,11 +1,8 @@ package dasearch import ( - "fmt" - "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // DASearchCmd groups Declarative Agent Search subcommands. @@ -21,17 +18,7 @@ func dasearchEndpoint() string { type DASearchAgentsCmd struct{} func (c *DASearchAgentsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(dasearchEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "M365_Copilot_Get_Available_Agents", map[string]any{}) - if err != nil { - return fmt.Errorf("list agents: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(dasearchEndpoint(), "M365_Copilot_Get_Available_Agents", "list agents", map[string]any{}) if err != nil { return err } diff --git a/internal/commands/excel/excel.go b/internal/commands/excel/excel.go index cc76cf9..76f7a07 100644 --- a/internal/commands/excel/excel.go +++ b/internal/commands/excel/excel.go @@ -5,7 +5,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // ExcelCmd groups all Excel subcommands. @@ -22,30 +21,26 @@ func excelEndpoint() string { // ExcelCreateCmd creates a new Excel workbook. type ExcelCreateCmd struct { - FileName string `arg:"" help:"Desired file name for the new workbook"` + FileName string `arg:"" help:"Desired file name for the new workbook"` + CSVContent string `help:"CSV content to populate the workbook" name:"csv-content" optional:"" default:""` } func (c *ExcelCreateCmd) Run(ctx *commands.Context) error { if ctx.DryRun { return ctx.ValidateDryRun(excelEndpoint(), "CreateWorkbook", fmt.Sprintf("create Excel workbook %q", c.FileName), - map[string]any{"action": "excel.create", "desiredFileName": c.FileName}, + map[string]any{ + "action": "excel.create", + "fileName": c.FileName, + "csvContent": c.CSVContent, + }, ) } - client := ctx.NewMCPClient(excelEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "CreateWorkbook", map[string]any{ - "desiredFileName": c.FileName, + data, err := ctx.CallToolData(excelEndpoint(), "CreateWorkbook", "create workbook", map[string]any{ + "fileName": c.FileName, + "csvContent": c.CSVContent, }) - if err != nil { - return fmt.Errorf("create workbook: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -54,25 +49,13 @@ func (c *ExcelCreateCmd) Run(ctx *commands.Context) error { // ExcelGetCmd gets Excel workbook content. type ExcelGetCmd struct { - DriveID string `arg:"" help:"Drive ID"` - DocumentID string `arg:"" help:"Document ID"` + URL string `arg:"" help:"SharePoint sharing URL for the workbook"` } func (c *ExcelGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(excelEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetDocumentContent", map[string]any{ - "driveId": c.DriveID, - "documentId": c.DocumentID, + data, err := ctx.CallToolData(excelEndpoint(), "GetDocumentContent", "get workbook content", map[string]any{ + "url": c.URL, }) - if err != nil { - return fmt.Errorf("get workbook content: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -91,26 +74,22 @@ func (c *ExcelCommentCmd) Run(ctx *commands.Context) error { if ctx.DryRun { return ctx.ValidateDryRun(excelEndpoint(), "CreateComment", fmt.Sprintf("add comment to workbook %s at cell %s", c.DocumentID, c.CellAddress), - map[string]any{"action": "excel.comment", "driveId": c.DriveID, "documentId": c.DocumentID, "cellAddress": c.CellAddress}, + map[string]any{ + "action": "excel.comment", + "driveId": c.DriveID, + "documentId": c.DocumentID, + "cellAddress": c.CellAddress, + "content": c.Text, + }, ) } - client := ctx.NewMCPClient(excelEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "CreateComment", map[string]any{ + data, err := ctx.CallToolData(excelEndpoint(), "CreateComment", "add comment", map[string]any{ "driveId": c.DriveID, "documentId": c.DocumentID, "cellAddress": c.CellAddress, - "text": c.Text, + "content": c.Text, }) - if err != nil { - return fmt.Errorf("add comment: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -129,26 +108,22 @@ func (c *ExcelReplyCmd) Run(ctx *commands.Context) error { if ctx.DryRun { return ctx.ValidateDryRun(excelEndpoint(), "ReplyToComment", fmt.Sprintf("reply to comment %s on workbook %s", c.CommentID, c.DocumentID), - map[string]any{"action": "excel.reply", "commentId": c.CommentID, "driveId": c.DriveID, "documentId": c.DocumentID}, + map[string]any{ + "action": "excel.reply", + "commentId": c.CommentID, + "driveId": c.DriveID, + "documentId": c.DocumentID, + "newComment": c.Text, + }, ) } - client := ctx.NewMCPClient(excelEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ReplyToComment", map[string]any{ + data, err := ctx.CallToolData(excelEndpoint(), "ReplyToComment", "reply to comment", map[string]any{ "commentId": c.CommentID, "driveId": c.DriveID, "documentId": c.DocumentID, - "text": c.Text, + "newComment": c.Text, }) - if err != nil { - return fmt.Errorf("reply to comment: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/excel/excel_test.go b/internal/commands/excel/excel_test.go index 2dbf77b..4fb5036 100644 --- a/internal/commands/excel/excel_test.go +++ b/internal/commands/excel/excel_test.go @@ -15,17 +15,18 @@ func TestExcelCreateCmd_DryRun(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "action": map[string]any{"type": "string"}, - "desiredFileName": map[string]any{"type": "string"}, + "action": map[string]any{"type": "string"}, + "fileName": map[string]any{"type": "string"}, + "csvContent": map[string]any{"type": "string"}, }, - "required": []any{"desiredFileName"}, + "required": []any{"fileName", "csvContent"}, }, }, } ctx, buf := testutil.SetupTestServerWithSchemas(t, nil, schemas) ctx.DryRun = true - cmd := &ExcelCreateCmd{FileName: "budget.xlsx"} + cmd := &ExcelCreateCmd{FileName: "budget.xlsx", CSVContent: "category,amount\ntravel,100"} if err := cmd.Run(ctx); err != nil { t.Fatalf("Run() error: %v", err) } @@ -57,8 +58,9 @@ func TestExcelCommentCmd_DryRun(t *testing.T) { "driveId": map[string]any{"type": "string"}, "documentId": map[string]any{"type": "string"}, "cellAddress": map[string]any{"type": "string"}, + "content": map[string]any{"type": "string"}, }, - "required": []any{"driveId", "documentId", "cellAddress"}, + "required": []any{"driveId", "documentId", "cellAddress", "content"}, }, }, } diff --git a/internal/commands/knowledge/knowledge.go b/internal/commands/knowledge/knowledge.go index e149014..f77648f 100644 --- a/internal/commands/knowledge/knowledge.go +++ b/internal/commands/knowledge/knowledge.go @@ -5,7 +5,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // KnowledgeCmd groups federated knowledge subcommands. @@ -28,20 +27,10 @@ type KnowledgeQueryCmd struct { } func (c *KnowledgeQueryCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(knowledgeEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "query_federated_knowledge", map[string]any{ + data, err := ctx.CallToolData(knowledgeEndpoint(), "query_federated_knowledge", "query", map[string]any{ "consumerId": c.ConsumerID, "query": c.Query, }) - if err != nil { - return fmt.Errorf("query: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -54,19 +43,9 @@ type KnowledgeListCmd struct { } func (c *KnowledgeListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(knowledgeEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "retrieve_federated_knowledge", map[string]any{ + data, err := ctx.CallToolData(knowledgeEndpoint(), "retrieve_federated_knowledge", "list", map[string]any{ "consumerId": c.ConsumerID, }) - if err != nil { - return fmt.Errorf("list: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -94,23 +73,13 @@ func (c *KnowledgeConfigureCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(knowledgeEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "configure_federated_knowledge", map[string]any{ + data, err := ctx.CallToolData(knowledgeEndpoint(), "configure_federated_knowledge", "configure", map[string]any{ "consumerId": c.ConsumerID, "knowledgeConfig": map[string]any{}, "sourceType": c.SourceType, "displayName": c.DisplayName, "description": c.Description, }) - if err != nil { - return fmt.Errorf("configure: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -135,20 +104,10 @@ func (c *KnowledgeIngestCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(knowledgeEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ingest_federated_knowledge", map[string]any{ + data, err := ctx.CallToolData(knowledgeEndpoint(), "ingest_federated_knowledge", "ingest", map[string]any{ "consumerId": c.ConsumerID, "searchConfigurationId": c.ConfigID, }) - if err != nil { - return fmt.Errorf("ingest: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -177,20 +136,10 @@ func (c *KnowledgeDeleteCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(knowledgeEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "delete_federated_knowledge", map[string]any{ + data, err := ctx.CallToolData(knowledgeEndpoint(), "delete_federated_knowledge", "delete", map[string]any{ "searchConfigurationId": c.ConfigID, "consumerId": c.ConsumerID, }) - if err != nil { - return fmt.Errorf("delete: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/mail/mail.go b/internal/commands/mail/mail.go index 2e0d56c..95ad484 100644 --- a/internal/commands/mail/mail.go +++ b/internal/commands/mail/mail.go @@ -44,11 +44,6 @@ type MailSearchCmd struct { } func (c *MailSearchCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - // Auto-wrap bare search terms into OData $search query parameters. // If the user provides a raw OData string (starting with ? or $), pass it through. query := c.Query @@ -56,28 +51,13 @@ func (c *MailSearchCmd) Run(ctx *commands.Context) error { query = `?$search="` + query + `"` } - resp, err := client.CallTool(ctx.Ctx, "SearchMessagesQueryParameters", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "SearchMessagesQueryParameters", "search", map[string]any{ "queryParameters": query, }) - if err != nil { - return fmt.Errorf("search: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } - rows := output.ToRows(data, "messages") - if rows == nil { - rows = output.ToRows(data, "value") - } - if rows == nil { - return ctx.Output.PrintItem(data) - } - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("messages", output.MailColumns, rows) + return ctx.Output.PrintListFromData("messages", output.MailColumns, data, c.Max, "messages", "value") } // MailSearchNLCmd searches emails with natural language. @@ -86,19 +66,9 @@ type MailSearchNLCmd struct { } func (c *MailSearchNLCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "SearchMessages", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "SearchMessages", "search", map[string]any{ "message": c.Query, }) - if err != nil { - return fmt.Errorf("search: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -111,19 +81,9 @@ type MailGetCmd struct { } func (c *MailGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetMessage", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "GetMessage", "get message", map[string]any{ "id": c.ID, }) - if err != nil { - return fmt.Errorf("get message: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -159,11 +119,6 @@ func (c *MailSendCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "to": c.To, "subject": c.Subject, @@ -176,12 +131,7 @@ func (c *MailSendCmd) Run(ctx *commands.Context) error { args["bcc"] = c.BCC } - resp, err := client.CallTool(ctx.Ctx, "SendEmailWithAttachments", args) - if err != nil { - return fmt.Errorf("send email: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(mailEndpoint(), "SendEmailWithAttachments", "send email", args) if err != nil { return err } @@ -204,21 +154,11 @@ func (c *MailReplyCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ReplyToMessage", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "ReplyToMessage", "reply", map[string]any{ "id": c.ID, "comment": c.Comment, "sendImmediately": c.Send, }) - if err != nil { - return fmt.Errorf("reply: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -245,21 +185,11 @@ func (c *MailReplyAllCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ReplyAllToMessage", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "ReplyAllToMessage", "reply-all", map[string]any{ "id": c.ID, "comment": c.Comment, "sendImmediately": c.Send, }) - if err != nil { - return fmt.Errorf("reply-all: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -293,11 +223,6 @@ func (c *MailForwardCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "messageId": c.ID, "additionalTo": c.To, @@ -306,12 +231,7 @@ func (c *MailForwardCmd) Run(ctx *commands.Context) error { args["introComment"] = c.Comment } - resp, err := client.CallTool(ctx.Ctx, "ForwardMessage", args) - if err != nil { - return fmt.Errorf("forward: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(mailEndpoint(), "ForwardMessage", "forward", args) if err != nil { return err } @@ -334,17 +254,7 @@ func (c *MailDeleteCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "DeleteMessage", map[string]any{"id": c.ID}) - if err != nil { - return fmt.Errorf("delete: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(mailEndpoint(), "DeleteMessage", "delete", map[string]any{"id": c.ID}) if err != nil { return err } @@ -365,20 +275,10 @@ func (c *MailFlagCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "FlagEmail", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "FlagEmail", "flag", map[string]any{ "messageId": c.ID, "flagStatus": c.Status, }) - if err != nil { - return fmt.Errorf("flag: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -400,11 +300,6 @@ func (c *MailDraftCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{} if c.Subject != "" { args["subject"] = c.Subject @@ -419,12 +314,7 @@ func (c *MailDraftCmd) Run(ctx *commands.Context) error { args["cc"] = c.CC } - resp, err := client.CallTool(ctx.Ctx, "CreateDraftMessage", args) - if err != nil { - return fmt.Errorf("create draft: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(mailEndpoint(), "CreateDraftMessage", "create draft", args) if err != nil { return err } @@ -444,17 +334,7 @@ func (c *MailSendDraftCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "SendDraftMessage", map[string]any{"id": c.ID}) - if err != nil { - return fmt.Errorf("send draft: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(mailEndpoint(), "SendDraftMessage", "send draft", map[string]any{"id": c.ID}) if err != nil { return err } @@ -467,19 +347,9 @@ type MailAttachmentsCmd struct { } func (c *MailAttachmentsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetAttachments", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "GetAttachments", "get attachments", map[string]any{ "messageId": c.ID, }) - if err != nil { - return fmt.Errorf("get attachments: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -503,11 +373,6 @@ func (c *MailUpdateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "id": c.ID, } @@ -524,12 +389,7 @@ func (c *MailUpdateCmd) Run(ctx *commands.Context) error { args["categories"] = c.Categories } - resp, err := client.CallTool(ctx.Ctx, "UpdateMessage", args) - if err != nil { - return fmt.Errorf("update message: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(mailEndpoint(), "UpdateMessage", "update message", args) if err != nil { return err } @@ -543,20 +403,10 @@ type MailDownloadCmd struct { } func (c *MailDownloadCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "DownloadAttachment", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "DownloadAttachment", "download attachment", map[string]any{ "messageId": c.MessageID, "attachmentId": c.AttachmentID, }) - if err != nil { - return fmt.Errorf("download attachment: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -588,26 +438,16 @@ func (c *MailUploadCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - toolName := "UploadAttachment" if c.Large { toolName = "UploadLargeAttachment" } - resp, err := client.CallTool(ctx.Ctx, toolName, map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), toolName, "upload attachment", map[string]any{ "messageId": c.MessageID, "fileName": c.FileName, "contentBase64": c.ContentBase64, }) - if err != nil { - return fmt.Errorf("upload attachment: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -631,20 +471,10 @@ func (c *MailDeleteAttachCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "DeleteAttachment", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "DeleteAttachment", "delete attachment", map[string]any{ "messageId": c.MessageID, "attachmentId": c.AttachmentID, }) - if err != nil { - return fmt.Errorf("delete attachment: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -669,11 +499,6 @@ func (c *MailUpdateDraftCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "messageId": c.MessageID, } @@ -693,12 +518,7 @@ func (c *MailUpdateDraftCmd) Run(ctx *commands.Context) error { args["body"] = c.Body } - resp, err := client.CallTool(ctx.Ctx, "UpdateDraft", args) - if err != nil { - return fmt.Errorf("update draft: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(mailEndpoint(), "UpdateDraft", "update draft", args) if err != nil { return err } @@ -719,20 +539,10 @@ func (c *MailDraftAttachCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "AddDraftAttachments", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "AddDraftAttachments", "add draft attachments", map[string]any{ "messageId": c.MessageID, "attachmentUris": c.AttachmentUris, }) - if err != nil { - return fmt.Errorf("add draft attachments: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -752,19 +562,9 @@ func (c *MailReplyThreadCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ReplyWithFullThread", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "ReplyWithFullThread", "reply with thread", map[string]any{ "messageId": c.MessageID, }) - if err != nil { - return fmt.Errorf("reply with thread: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -784,19 +584,9 @@ func (c *MailReplyAllThreadCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ReplyAllWithFullThread", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "ReplyAllWithFullThread", "reply-all with thread", map[string]any{ "messageId": c.MessageID, }) - if err != nil { - return fmt.Errorf("reply-all with thread: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -816,19 +606,9 @@ func (c *MailForwardThreadCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(mailEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ForwardMessageWithFullThread", map[string]any{ + data, err := ctx.CallToolData(mailEndpoint(), "ForwardMessageWithFullThread", "forward with thread", map[string]any{ "messageId": c.MessageID, }) - if err != nil { - return fmt.Errorf("forward with thread: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/me/me.go b/internal/commands/me/me.go index 23966d4..bc2bd19 100644 --- a/internal/commands/me/me.go +++ b/internal/commands/me/me.go @@ -1,8 +1,6 @@ package me import ( - "fmt" - "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" "github.com/sozercan/a365cli/internal/output" @@ -25,17 +23,7 @@ func meEndpoint() string { type MeWhoamiCmd struct{} func (c *MeWhoamiCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(meEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetMyDetails", map[string]any{}) - if err != nil { - return fmt.Errorf("get my details: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(meEndpoint(), "GetMyDetails", "get my details", map[string]any{}) if err != nil { return err } @@ -48,19 +36,9 @@ type MeGetCmd struct { } func (c *MeGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(meEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetUserDetails", map[string]any{ + data, err := ctx.CallToolData(meEndpoint(), "GetUserDetails", "get user", map[string]any{ "userIdentifier": c.User, }) - if err != nil { - return fmt.Errorf("get user: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -73,31 +51,14 @@ type MeSearchCmd struct { } func (c *MeSearchCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(meEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetMultipleUsersDetails", map[string]any{ + data, err := ctx.CallToolData(meEndpoint(), "GetMultipleUsersDetails", "search users", map[string]any{ "searchValues": c.Query, }) - if err != nil { - return fmt.Errorf("search users: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } - rows := output.ToRows(data, "users") - if rows == nil { - rows = output.ToRows(data, "value") - } - if rows == nil { - return ctx.Output.PrintItem(data) - } - return ctx.Output.PrintList("users", output.UserColumns, rows) + return ctx.Output.PrintListFromData("users", output.UserColumns, data, 0, "users", "value") } // MeManagerCmd gets a user's manager. @@ -106,19 +67,9 @@ type MeManagerCmd struct { } func (c *MeManagerCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(meEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetManagerDetails", map[string]any{ + data, err := ctx.CallToolData(meEndpoint(), "GetManagerDetails", "get manager", map[string]any{ "userId": c.UserID, }) - if err != nil { - return fmt.Errorf("get manager: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -131,29 +82,12 @@ type MeReportsCmd struct { } func (c *MeReportsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(meEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetDirectReportsDetails", map[string]any{ + data, err := ctx.CallToolData(meEndpoint(), "GetDirectReportsDetails", "get reports", map[string]any{ "userId": c.UserID, }) - if err != nil { - return fmt.Errorf("get reports: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } - rows := output.ToRows(data, "directReports") - if rows == nil { - rows = output.ToRows(data, "value") - } - if rows == nil { - return ctx.Output.PrintItem(data) - } - return ctx.Output.PrintList("directReports", output.UserColumns, rows) + return ctx.Output.PrintListFromData("directReports", output.UserColumns, data, 0, "directReports", "value") } diff --git a/internal/commands/nlweb/nlweb.go b/internal/commands/nlweb/nlweb.go index 05d75a1..fd1ce15 100644 --- a/internal/commands/nlweb/nlweb.go +++ b/internal/commands/nlweb/nlweb.go @@ -1,11 +1,8 @@ package nlweb import ( - "fmt" - "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // NLWebCmd groups NLWeb search subcommands. @@ -25,19 +22,9 @@ type NLWebAskCmd struct { } func (c *NLWebAskCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(nlwebEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ask", map[string]any{ + data, err := ctx.CallToolData(nlwebEndpoint(), "ask", "ask", map[string]any{ "query": c.Query, }) - if err != nil { - return fmt.Errorf("ask: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -50,19 +37,9 @@ type NLWebWhoCmd struct { } func (c *NLWebWhoCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(nlwebEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "who", map[string]any{ + data, err := ctx.CallToolData(nlwebEndpoint(), "who", "who", map[string]any{ "query": c.Query, }) - if err != nil { - return fmt.Errorf("who: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -73,17 +50,7 @@ func (c *NLWebWhoCmd) Run(ctx *commands.Context) error { type NLWebSitesCmd struct{} func (c *NLWebSitesCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(nlwebEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "list_sites", map[string]any{}) - if err != nil { - return fmt.Errorf("list sites: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(nlwebEndpoint(), "list_sites", "list sites", map[string]any{}) if err != nil { return err } diff --git a/internal/commands/onedriveremote/onedriveremote.go b/internal/commands/onedriveremote/onedriveremote.go index 42c0b8c..eff8d47 100644 --- a/internal/commands/onedriveremote/onedriveremote.go +++ b/internal/commands/onedriveremote/onedriveremote.go @@ -5,7 +5,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // odrEndpoint returns the agent365 endpoint for the OneDrive Remote MCP server. @@ -35,17 +34,7 @@ type OneDriveRemoteCmd struct { type ODRInfoCmd struct{} func (c *ODRInfoCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "getOnedrive", map[string]any{}) - if err != nil { - return fmt.Errorf("get OneDrive info: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(odrEndpoint(), "getOnedrive", "get OneDrive info", map[string]any{}) if err != nil { return err } @@ -58,17 +47,7 @@ func (c *ODRInfoCmd) Run(ctx *commands.Context) error { type ODRLsCmd struct{} func (c *ODRLsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "getFolderChildrenInMyOnedrive", map[string]any{}) - if err != nil { - return fmt.Errorf("list folder: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(odrEndpoint(), "getFolderChildrenInMyOnedrive", "list folder", map[string]any{}) if err != nil { return err } @@ -83,19 +62,9 @@ type ODRSearchCmd struct { } func (c *ODRSearchCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "findFileOrFolderInMyDrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "findFileOrFolderInMyDrive", "search", map[string]any{ "searchQuery": c.Query, }) - if err != nil { - return fmt.Errorf("search: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -111,11 +80,6 @@ type ODRGetCmd struct { } func (c *ODRGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - var toolName string args := map[string]any{} @@ -129,12 +93,7 @@ func (c *ODRGetCmd) Run(ctx *commands.Context) error { } } - resp, err := client.CallTool(ctx.Ctx, toolName, args) - if err != nil { - return fmt.Errorf("get metadata: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(odrEndpoint(), toolName, "get metadata", args) if err != nil { return err } @@ -149,19 +108,9 @@ type ODRCatCmd struct { } func (c *ODRCatCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "readSmallTextFileFromMyOnedrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "readSmallTextFileFromMyOnedrive", "read file", map[string]any{ "fileId": c.FileID, }) - if err != nil { - return fmt.Errorf("read file: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -186,19 +135,9 @@ func (c *ODRMkdirCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "createFolderInMyOnedrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "createFolderInMyOnedrive", "create folder", map[string]any{ "folderName": c.FolderName, }) - if err != nil { - return fmt.Errorf("create folder: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -225,20 +164,10 @@ func (c *ODRWriteCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "createSmallTextFileInMyOnedrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "createSmallTextFileInMyOnedrive", "create file", map[string]any{ "filename": c.Filename, "contentText": c.ContentText, }) - if err != nil { - return fmt.Errorf("create file: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -262,26 +191,16 @@ func (c *ODRRenameCmd) Run(ctx *commands.Context) error { "action": "onedrive-remote.rename", "fileOrFolderId": c.FileOrFolderID, "newFileOrFolderName": c.NewFileOrFolderName, - "etag": c.Etag, + "etag": c.Etag, }, ) } - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "renameFileOrFolderInMyOnedrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "renameFileOrFolderInMyOnedrive", "rename", map[string]any{ "fileOrFolderId": c.FileOrFolderID, "newFileOrFolderName": c.NewFileOrFolderName, "etag": c.Etag, }) - if err != nil { - return fmt.Errorf("rename: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -305,26 +224,16 @@ func (c *ODRMvCmd) Run(ctx *commands.Context) error { "action": "onedrive-remote.mv", "fileId": c.FileID, "newParentFolderId": c.NewParentFolderID, - "etag": c.Etag, + "etag": c.Etag, }, ) } - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "moveSmallFileInMyOnedrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "moveSmallFileInMyOnedrive", "move file", map[string]any{ "fileId": c.FileID, "newParentFolderId": c.NewParentFolderID, "etag": c.Etag, }) - if err != nil { - return fmt.Errorf("move file: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -355,20 +264,10 @@ func (c *ODRRmCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "deleteFileOrFolderInMyOnedrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "deleteFileOrFolderInMyOnedrive", "delete", map[string]any{ "fileOrFolderId": c.FileOrFolderID, "etag": c.Etag, }) - if err != nil { - return fmt.Errorf("delete: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -397,21 +296,11 @@ func (c *ODRShareCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "shareFileOrFolderInMyOnedrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "shareFileOrFolderInMyOnedrive", "share", map[string]any{ "fileOrFolderId": c.FileOrFolderID, "recipientEmails": c.RecipientEmails, "roles": c.Roles, }) - if err != nil { - return fmt.Errorf("share: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -438,20 +327,10 @@ func (c *ODRLabelCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(odrEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "setSensitivityLabelOnFileInMyOnedrive", map[string]any{ + data, err := ctx.CallToolData(odrEndpoint(), "setSensitivityLabelOnFileInMyOnedrive", "set label", map[string]any{ "fileId": c.FileID, "sensitivityLabelId": c.SensitivityLabelID, }) - if err != nil { - return fmt.Errorf("set label: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/planner/planner.go b/internal/commands/planner/planner.go index 3e4338c..89f2580 100644 --- a/internal/commands/planner/planner.go +++ b/internal/commands/planner/planner.go @@ -10,9 +10,9 @@ import ( // PlannerCmd groups all Planner subcommands. type PlannerCmd struct { - Plans PlansCmd `cmd:"" help:"Planner plans"` - Tasks TasksCmd `cmd:"" help:"Planner tasks"` - Goals GoalsCmd `cmd:"" help:"Planner goals"` + Plans PlansCmd `cmd:"" help:"Planner plans"` + Tasks TasksCmd `cmd:"" help:"Planner tasks"` + Goals GoalsCmd `cmd:"" help:"Planner goals"` } func plannerEndpoint() string { @@ -34,29 +34,11 @@ type PlansListCmd struct { } func (c *PlansListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "QueryPlans", map[string]any{}) - if err != nil { - return fmt.Errorf("list plans: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "QueryPlans", "list plans", map[string]any{}) if err != nil { return err } - rows := output.ToRows(data, "plans") - if rows == nil { - rows = output.ToRows(data, "value") - } - if rows == nil { - return ctx.Output.PrintItem(data) - } - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("plans", output.PlannerPlanColumns, rows) + return ctx.Output.PrintListFromData("plans", output.PlannerPlanColumns, data, c.Max, "plans", "value") } type PlansGetCmd struct { @@ -64,15 +46,7 @@ type PlansGetCmd struct { } func (c *PlansGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "GetPlan", map[string]any{"planId": c.ID}) - if err != nil { - return fmt.Errorf("get plan: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "GetPlan", "get plan", map[string]any{"planId": c.ID}) if err != nil { return err } @@ -88,15 +62,7 @@ func (c *PlansCreateCmd) Run(ctx *commands.Context) error { return ctx.ValidateDryRun(plannerEndpoint(), "CreatePlan", fmt.Sprintf("create plan %q", c.Title), map[string]any{"action": "planner.create-plan", "title": c.Title}) } - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "CreatePlan", map[string]any{"title": c.Title}) - if err != nil { - return fmt.Errorf("create plan: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "CreatePlan", "create plan", map[string]any{"title": c.Title}) if err != nil { return err } @@ -113,19 +79,12 @@ func (c *PlansUpdateCmd) Run(ctx *commands.Context) error { return ctx.ValidateDryRun(plannerEndpoint(), "UpdatePlan", fmt.Sprintf("update plan %s", c.ID), map[string]any{"action": "planner.update-plan", "planId": c.ID}) } - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } + args := map[string]any{"planId": c.ID} if c.Title != "" { args["title"] = c.Title } - resp, err := client.CallTool(ctx.Ctx, "UpdatePlan", args) - if err != nil { - return fmt.Errorf("update plan: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "UpdatePlan", "update plan", args) if err != nil { return err } @@ -147,29 +106,11 @@ type TasksListCmd struct { } func (c *TasksListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "QueryTasksInPlan", map[string]any{"planId": c.PlanID}) - if err != nil { - return fmt.Errorf("list tasks: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "QueryTasksInPlan", "list tasks", map[string]any{"planId": c.PlanID}) if err != nil { return err } - rows := output.ToRows(data, "tasks") - if rows == nil { - rows = output.ToRows(data, "value") - } - if rows == nil { - return ctx.Output.PrintItem(data) - } - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("tasks", output.PlannerTaskColumns, rows) + return ctx.Output.PrintListFromData("tasks", output.PlannerTaskColumns, data, c.Max, "tasks", "value") } type TasksGetCmd struct { @@ -177,15 +118,7 @@ type TasksGetCmd struct { } func (c *TasksGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "GetTask", map[string]any{"taskId": c.ID}) - if err != nil { - return fmt.Errorf("get task: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "GetTask", "get task", map[string]any{"taskId": c.ID}) if err != nil { return err } @@ -202,17 +135,9 @@ func (c *TasksCreateCmd) Run(ctx *commands.Context) error { return ctx.ValidateDryRun(plannerEndpoint(), "CreateTask", fmt.Sprintf("create task %q in plan %s", c.Title, c.PlanID), map[string]any{"action": "planner.create-task", "planId": c.PlanID, "title": c.Title}) } - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "CreateTask", map[string]any{ + data, err := ctx.CallToolData(plannerEndpoint(), "CreateTask", "create task", map[string]any{ "planId": c.PlanID, "title": c.Title, }) - if err != nil { - return fmt.Errorf("create task: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -229,19 +154,12 @@ func (c *TasksUpdateCmd) Run(ctx *commands.Context) error { return ctx.ValidateDryRun(plannerEndpoint(), "UpdateTask", fmt.Sprintf("update task %s", c.ID), map[string]any{"action": "planner.update-task", "taskId": c.ID}) } - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } + args := map[string]any{"taskId": c.ID} if c.Title != "" { args["title"] = c.Title } - resp, err := client.CallTool(ctx.Ctx, "UpdateTask", args) - if err != nil { - return fmt.Errorf("update task: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "UpdateTask", "update task", args) if err != nil { return err } @@ -262,15 +180,7 @@ type GoalsListCmd struct { } func (c *GoalsListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "QueryGoalsInPlan", map[string]any{"planId": c.PlanID}) - if err != nil { - return fmt.Errorf("list goals: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "QueryGoalsInPlan", "list goals", map[string]any{"planId": c.PlanID}) if err != nil { return err } @@ -282,15 +192,7 @@ type GoalsGetCmd struct { } func (c *GoalsGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "GetGoal", map[string]any{"goalId": c.ID}) - if err != nil { - return fmt.Errorf("get goal: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "GetGoal", "get goal", map[string]any{"goalId": c.ID}) if err != nil { return err } @@ -307,17 +209,9 @@ func (c *GoalsCreateCmd) Run(ctx *commands.Context) error { return ctx.ValidateDryRun(plannerEndpoint(), "CreateGoal", fmt.Sprintf("create goal %q in plan %s", c.Title, c.PlanID), map[string]any{"action": "planner.create-goal", "planId": c.PlanID, "title": c.Title}) } - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "CreateGoal", map[string]any{ + data, err := ctx.CallToolData(plannerEndpoint(), "CreateGoal", "create goal", map[string]any{ "planId": c.PlanID, "title": c.Title, }) - if err != nil { - return fmt.Errorf("create goal: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -334,19 +228,12 @@ func (c *GoalsUpdateCmd) Run(ctx *commands.Context) error { return ctx.ValidateDryRun(plannerEndpoint(), "UpdateGoal", fmt.Sprintf("update goal %s", c.ID), map[string]any{"action": "planner.update-goal", "goalId": c.ID}) } - client := ctx.NewMCPClient(plannerEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } + args := map[string]any{"goalId": c.ID} if c.Title != "" { args["title"] = c.Title } - resp, err := client.CallTool(ctx.Ctx, "UpdateGoal", args) - if err != nil { - return fmt.Errorf("update goal: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(plannerEndpoint(), "UpdateGoal", "update goal", args) if err != nil { return err } diff --git a/internal/commands/root.go b/internal/commands/root.go index 08a57ea..d6e5257 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -39,6 +39,27 @@ func (c *Context) NewMCPClient(endpoint string) *mcp.Client { return client } +// CallToolData initializes the MCP server, invokes one tool, and extracts the +// domain data from the MCP response. It centralizes the standard command +// execution path so handlers only describe the tool name, arguments, and +// rendering behavior. +func (c *Context) CallToolData(endpoint, toolName, action string, args map[string]any) (map[string]any, error) { + client := c.NewMCPClient(endpoint) + if err := client.Initialize(c.Ctx); err != nil { + return nil, fmt.Errorf("initialize: %w", err) + } + + resp, err := client.CallTool(c.Ctx, toolName, args) + if err != nil { + if action == "" { + action = toolName + } + return nil, fmt.Errorf("%s: %w", action, err) + } + + return output.ExtractContent(resp) +} + // EnsureAuth checks that the user is authenticated and sets up the token provider. // If no cached auth record exists (e.g., after logout), it performs a full // interactive login to re-establish credentials. diff --git a/internal/commands/root_test.go b/internal/commands/root_test.go index 8b07546..441f2f6 100644 --- a/internal/commands/root_test.go +++ b/internal/commands/root_test.go @@ -73,8 +73,112 @@ func TestNewMCPClient_Verbose(t *testing.T) { // doesn't panic or error when created with Verbose=true. } -// setupMockMCPServer creates an httptest server that handles initialize, tools/list, -// and tools/call for ValidateDryRun tests. +func TestCallToolData_Success(t *testing.T) { + var gotTool string + var gotArgs map[string]any + server := setupMockMCPCallServer(t, func(w http.ResponseWriter, reqID int, params toolCallParams) { + gotTool = params.Name + gotArgs = params.Arguments + writeMCPSSE(t, w, reqID, map[string]any{ + "result": map[string]any{ + "content": []map[string]any{ + {"type": "text", "text": `{"results":[{"title":"Contoso"}]}`}, + }, + }, + }) + }) + + ctx := &Context{ + Ctx: context.Background(), + TokenProvider: func(ctx context.Context) (string, error) { return "test-token", nil }, + Output: &output.Formatter{Format: output.FormatJSON}, + } + + data, err := ctx.CallToolData(server.URL+"/", "SearchWeb", "search web", map[string]any{"query": "contoso"}) + if err != nil { + t.Fatalf("CallToolData() error: %v", err) + } + if gotTool != "SearchWeb" { + t.Fatalf("tool = %q, want SearchWeb", gotTool) + } + if gotArgs["query"] != "contoso" { + t.Fatalf("query arg = %v, want contoso", gotArgs["query"]) + } + results, ok := data["results"].([]any) + if !ok || len(results) != 1 { + t.Fatalf("results = %#v, want one result", data["results"]) + } +} + +func TestCallToolData_ToolCallErrorIncludesAction(t *testing.T) { + server := setupMockMCPCallServer(t, func(w http.ResponseWriter, _ int, _ toolCallParams) { + http.Error(w, "upstream failed", http.StatusInternalServerError) + }) + + ctx := &Context{ + Ctx: context.Background(), + TokenProvider: func(ctx context.Context) (string, error) { return "test-token", nil }, + Output: &output.Formatter{Format: output.FormatJSON}, + } + + _, err := ctx.CallToolData(server.URL+"/", "SearchWeb", "search web", map[string]any{"query": "contoso"}) + if err == nil { + t.Fatal("CallToolData() expected error") + } + if got := err.Error(); !strings.Contains(got, "search web: HTTP 500") { + t.Fatalf("error = %q, want action and HTTP status", got) + } +} + +type toolCallParams struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments"` +} + +func setupMockMCPCallServer(t *testing.T, onToolCall func(http.ResponseWriter, int, toolCallParams)) *httptest.Server { + t.Helper() + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + var req struct { + ID int `json:"id"` + Method string `json:"method"` + Params json.RawMessage `json:"params"` + } + json.Unmarshal(body, &req) //nolint:errcheck + + switch req.Method { + case "initialize": + writeMCPSSE(t, w, req.ID, map[string]any{ + "result": map[string]any{ + "protocolVersion": "2024-11-05", + "serverInfo": map[string]any{"name": "test", "version": "1.0"}, + }, + }) + case "tools/call": + var params toolCallParams + json.Unmarshal(req.Params, ¶ms) //nolint:errcheck + onToolCall(w, req.ID, params) + default: + writeMCPSSE(t, w, req.ID, map[string]any{ + "error": map[string]any{"code": -32601, "message": "unknown method"}, + }) + } + })) + t.Cleanup(func() { server.Close() }) + return server +} + +func writeMCPSSE(t *testing.T, w http.ResponseWriter, reqID int, payload map[string]any) { + t.Helper() + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Mcp-Session-Id", "test-session") + payload["jsonrpc"] = "2.0" + payload["id"] = reqID + fmt.Fprintf(w, "event: message\ndata: %s\n\n", mustJSON(payload)) +} + +// setupMockMCPServer creates an httptest server that handles initialize and tools/list +// for ValidateDryRun tests. func setupMockMCPServer(t *testing.T, toolSchemas []mcp.ToolInfo) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/commands/sharepoint/sharepoint.go b/internal/commands/sharepoint/sharepoint.go index 2387d5f..46311a2 100644 --- a/internal/commands/sharepoint/sharepoint.go +++ b/internal/commands/sharepoint/sharepoint.go @@ -5,7 +5,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // spEndpoint returns the agent365 endpoint for the SharePoint MCP server. @@ -42,11 +41,6 @@ type SPFindSiteCmd struct { } func (c *SPFindSiteCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{} if c.Query != "" { args["searchQuery"] = c.Query @@ -55,12 +49,7 @@ func (c *SPFindSiteCmd) Run(ctx *commands.Context) error { args["siteUrl"] = c.SiteUrl } - resp, err := client.CallTool(ctx.Ctx, "findSite", args) - if err != nil { - return fmt.Errorf("find site: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spEndpoint(), "findSite", "find site", args) if err != nil { return err } @@ -77,11 +66,6 @@ type SPListLibsCmd struct { } func (c *SPListLibsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{} if c.SiteID != "" { args["siteId"] = c.SiteID @@ -97,12 +81,7 @@ func (c *SPListLibsCmd) Run(ctx *commands.Context) error { toolName = "listDocumentLibrariesInSite" } - resp, err := client.CallTool(ctx.Ctx, toolName, args) - if err != nil { - return fmt.Errorf("list libraries: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spEndpoint(), toolName, "list libraries", args) if err != nil { return err } @@ -119,11 +98,6 @@ type SPListCmd struct { } func (c *SPListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "driveId": c.DriveID, } @@ -134,12 +108,7 @@ func (c *SPListCmd) Run(ctx *commands.Context) error { args["folderId"] = c.FolderID } - resp, err := client.CallTool(ctx.Ctx, "getFolderChildren", args) - if err != nil { - return fmt.Errorf("list folder: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spEndpoint(), "getFolderChildren", "list folder", args) if err != nil { return err } @@ -157,11 +126,6 @@ type SPGetCmd struct { } func (c *SPGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - var toolName string args := map[string]any{} @@ -181,12 +145,7 @@ func (c *SPGetCmd) Run(ctx *commands.Context) error { } } - resp, err := client.CallTool(ctx.Ctx, toolName, args) - if err != nil { - return fmt.Errorf("get metadata: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spEndpoint(), toolName, "get metadata", args) if err != nil { return err } @@ -204,11 +163,6 @@ type SPCatCmd struct { } func (c *SPCatCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "driveId": c.DriveID, } @@ -226,12 +180,7 @@ func (c *SPCatCmd) Run(ctx *commands.Context) error { toolName = "readSmallTextFile" } - resp, err := client.CallTool(ctx.Ctx, toolName, args) - if err != nil { - return fmt.Errorf("read file: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spEndpoint(), toolName, "read file", args) if err != nil { return err } @@ -248,11 +197,6 @@ type SPSearchCmd struct { } func (c *SPSearchCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "searchQuery": c.Query, } @@ -263,12 +207,7 @@ func (c *SPSearchCmd) Run(ctx *commands.Context) error { args["siteId"] = c.SiteID } - resp, err := client.CallTool(ctx.Ctx, "findFileOrFolder", args) - if err != nil { - return fmt.Errorf("search: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spEndpoint(), "findFileOrFolder", "search", args) if err != nil { return err } @@ -297,21 +236,11 @@ func (c *SPMkdirCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "createFolder", map[string]any{ + data, err := ctx.CallToolData(spEndpoint(), "createFolder", "create folder", map[string]any{ "driveId": c.DriveID, "parentPath": c.ParentPath, "folderName": c.FolderName, }) - if err != nil { - return fmt.Errorf("create folder: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -342,11 +271,6 @@ func (c *SPWriteCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - var toolName string args := map[string]any{ "driveId": c.DriveID, @@ -362,12 +286,7 @@ func (c *SPWriteCmd) Run(ctx *commands.Context) error { args["content"] = c.Content } - resp, err := client.CallTool(ctx.Ctx, toolName, args) - if err != nil { - return fmt.Errorf("create file: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spEndpoint(), toolName, "create file", args) if err != nil { return err } @@ -398,22 +317,12 @@ func (c *SPUploadCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "uploadFileFromUrl", map[string]any{ + data, err := ctx.CallToolData(spEndpoint(), "uploadFileFromUrl", "upload file", map[string]any{ "sourceUrl": c.SourceURL, "destinationDriveId": c.DestinationDriveID, "destinationFolderPath": c.DestinationFolderPath, "fileName": c.FileName, }) - if err != nil { - return fmt.Errorf("upload file: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -454,11 +363,6 @@ func (c *SPDeleteCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "driveId": c.DriveID, } @@ -469,12 +373,7 @@ func (c *SPDeleteCmd) Run(ctx *commands.Context) error { args["itemId"] = c.ItemID } - resp, err := client.CallTool(ctx.Ctx, "deleteFileOrFolder", args) - if err != nil { - return fmt.Errorf("delete: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spEndpoint(), "deleteFileOrFolder", "delete", args) if err != nil { return err } @@ -505,22 +404,12 @@ func (c *SPMoveCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "moveFileOrFolder", map[string]any{ + data, err := ctx.CallToolData(spEndpoint(), "moveFileOrFolder", "move", map[string]any{ "sourceDriveId": c.SourceDriveID, "sourceItemPath": c.SourceItemPath, "destinationDriveId": c.DestinationDriveID, "destinationFolderPath": c.DestinationFolderPath, }) - if err != nil { - return fmt.Errorf("move: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -551,22 +440,12 @@ func (c *SPCopyCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "copyFileOrFolder", map[string]any{ + data, err := ctx.CallToolData(spEndpoint(), "copyFileOrFolder", "copy", map[string]any{ "sourceDriveId": c.SourceDriveID, "sourceItemPath": c.SourceItemPath, "destinationDriveId": c.DestinationDriveID, "destinationFolderPath": c.DestinationFolderPath, }) - if err != nil { - return fmt.Errorf("copy: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -595,21 +474,11 @@ func (c *SPRenameCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "renameFileOrFolder", map[string]any{ + data, err := ctx.CallToolData(spEndpoint(), "renameFileOrFolder", "rename", map[string]any{ "driveId": c.DriveID, "itemPath": c.ItemPath, "newName": c.NewName, }) - if err != nil { - return fmt.Errorf("rename: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -640,22 +509,12 @@ func (c *SPShareCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "shareFileOrFolder", map[string]any{ + data, err := ctx.CallToolData(spEndpoint(), "shareFileOrFolder", "share", map[string]any{ "driveId": c.DriveID, "itemPath": c.ItemPath, "type": c.Type, "scope": c.Scope, }) - if err != nil { - return fmt.Errorf("share: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -684,21 +543,11 @@ func (c *SPLabelCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "setSensitivityLabelOnFile", map[string]any{ + data, err := ctx.CallToolData(spEndpoint(), "setSensitivityLabelOnFile", "set label", map[string]any{ "driveId": c.DriveID, "itemId": c.ItemID, "labelId": c.LabelID, }) - if err != nil { - return fmt.Errorf("set label: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -713,19 +562,9 @@ type SPStatusCmd struct { } func (c *SPStatusCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "checkOperationStatus", map[string]any{ + data, err := ctx.CallToolData(spEndpoint(), "checkOperationStatus", "check status", map[string]any{ "operationUrl": c.OperationURL, }) - if err != nil { - return fmt.Errorf("check status: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/splists/splists.go b/internal/commands/splists/splists.go index 0ebf6bc..241c507 100644 --- a/internal/commands/splists/splists.go +++ b/internal/commands/splists/splists.go @@ -6,7 +6,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // SPListsCmd groups all SharePoint Lists subcommands. @@ -38,19 +37,11 @@ type SPLSitesCmd struct { } func (c *SPLSitesCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } args := map[string]any{} if c.Query != "" { args["searchQuery"] = c.Query } - resp, err := client.CallTool(ctx.Ctx, "searchSitesByName", args) - if err != nil { - return fmt.Errorf("search sites: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spListsEndpoint(), "searchSitesByName", "search sites", args) if err != nil { return err } @@ -66,18 +57,10 @@ type SPLSiteCmd struct { } func (c *SPLSiteCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "getSiteByPath", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "getSiteByPath", "get site", map[string]any{ "hostname": c.Hostname, "serverRelativePath": c.ServerRelativePath, }) - if err != nil { - return fmt.Errorf("get site: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -92,17 +75,9 @@ type SPLSubsitesCmd struct { } func (c *SPLSubsitesCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "listSubsites", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "listSubsites", "list subsites", map[string]any{ "siteId": c.SiteID, }) - if err != nil { - return fmt.Errorf("list subsites: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -117,17 +92,9 @@ type SPLListsCmd struct { } func (c *SPLListsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "listLists", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "listLists", "list lists", map[string]any{ "siteId": c.SiteID, }) - if err != nil { - return fmt.Errorf("list lists: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -143,18 +110,10 @@ type SPLItemsCmd struct { } func (c *SPLItemsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "listListItems", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "listListItems", "list items", map[string]any{ "siteId": c.SiteID, "listId": c.ListID, }) - if err != nil { - return fmt.Errorf("list items: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -170,18 +129,10 @@ type SPLColumnsCmd struct { } func (c *SPLColumnsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "listListColumns", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "listListColumns", "list columns", map[string]any{ "siteId": c.SiteID, "listId": c.ListID, }) - if err != nil { - return fmt.Errorf("list columns: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -203,18 +154,10 @@ func (c *SPLCreateCmd) Run(ctx *commands.Context) error { map[string]any{"action": "sp-lists.create-list", "siteId": c.SiteID, "displayName": c.DisplayName}, ) } - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "createList", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "createList", "create list", map[string]any{ "siteId": c.SiteID, "displayName": c.DisplayName, }) - if err != nil { - return fmt.Errorf("create list: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -238,20 +181,12 @@ func (c *SPLAddColumnCmd) Run(ctx *commands.Context) error { map[string]any{"action": "sp-lists.add-column", "siteId": c.SiteID, "listId": c.ListID, "name": c.Name, "columnType": c.ColumnType}, ) } - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "createListColumn", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "createListColumn", "add column", map[string]any{ "siteId": c.SiteID, "listId": c.ListID, "name": c.Name, "columnType": c.ColumnType, }) - if err != nil { - return fmt.Errorf("add column: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -279,19 +214,11 @@ func (c *SPLAddItemCmd) Run(ctx *commands.Context) error { map[string]any{"action": "sp-lists.add-item", "siteId": c.SiteID, "listId": c.ListID, "fields": fields}, ) } - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "createListItem", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "createListItem", "add item", map[string]any{ "siteId": c.SiteID, "listId": c.ListID, "fields": fields, }) - if err != nil { - return fmt.Errorf("add item: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -315,10 +242,7 @@ func (c *SPLUpdateItemCmd) Run(ctx *commands.Context) error { map[string]any{"action": "sp-lists.update-item", "siteId": c.SiteID, "listId": c.ListID, "itemId": c.ItemID}, ) } - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } + args := map[string]any{ "siteId": c.SiteID, "listId": c.ListID, @@ -331,11 +255,7 @@ func (c *SPLUpdateItemCmd) Run(ctx *commands.Context) error { } args["fields"] = fields } - resp, err := client.CallTool(ctx.Ctx, "updateListItem", args) - if err != nil { - return fmt.Errorf("update item: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spListsEndpoint(), "updateListItem", "update item", args) if err != nil { return err } @@ -359,10 +279,7 @@ func (c *SPLEditColCmd) Run(ctx *commands.Context) error { map[string]any{"action": "sp-lists.edit-column", "siteId": c.SiteID, "listId": c.ListID, "columnId": c.ColumnID}, ) } - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } + args := map[string]any{ "siteId": c.SiteID, "listId": c.ListID, @@ -371,11 +288,7 @@ func (c *SPLEditColCmd) Run(ctx *commands.Context) error { if c.Name != "" { args["name"] = c.Name } - resp, err := client.CallTool(ctx.Ctx, "editListColumn", args) - if err != nil { - return fmt.Errorf("edit column: %w", err) - } - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(spListsEndpoint(), "editListColumn", "edit column", args) if err != nil { return err } @@ -401,19 +314,11 @@ func (c *SPLDeleteItemCmd) Run(ctx *commands.Context) error { if err := ctx.Confirm(fmt.Sprintf("delete item %s from list %s", c.ItemID, c.ListID)); err != nil { return err } - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "deleteListItem", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "deleteListItem", "delete item", map[string]any{ "siteId": c.SiteID, "listId": c.ListID, "itemId": c.ItemID, }) - if err != nil { - return fmt.Errorf("delete item: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -439,19 +344,11 @@ func (c *SPLDeleteColCmd) Run(ctx *commands.Context) error { if err := ctx.Confirm(fmt.Sprintf("delete column %s from list %s", c.ColumnID, c.ListID)); err != nil { return err } - client := ctx.NewMCPClient(spListsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - resp, err := client.CallTool(ctx.Ctx, "deleteListColumn", map[string]any{ + data, err := ctx.CallToolData(spListsEndpoint(), "deleteListColumn", "delete column", map[string]any{ "siteId": c.SiteID, "listId": c.ListID, "columnId": c.ColumnID, }) - if err != nil { - return fmt.Errorf("delete column: %w", err) - } - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/teams/channels.go b/internal/commands/teams/channels.go index fc39ad5..2322b87 100644 --- a/internal/commands/teams/channels.go +++ b/internal/commands/teams/channels.go @@ -29,27 +29,13 @@ type ChannelsListCmd struct { } func (c *ChannelsListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ListChannels", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "ListChannels", "list channels", map[string]any{ "teamId": c.TeamID, }) - if err != nil { - return fmt.Errorf("list channels: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } - rows := output.ToRows(data, "channels") - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("channels", output.ChannelsColumns, rows) + return ctx.Output.PrintListFromData("channels", output.ChannelsColumns, data, c.Max) } // ChannelsGetCmd gets a specific channel. @@ -59,20 +45,10 @@ type ChannelsGetCmd struct { } func (c *ChannelsGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetChannel", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "GetChannel", "get channel", map[string]any{ "teamId": c.TeamID, "channelId": c.ChannelID, }) - if err != nil { - return fmt.Errorf("get channel: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -87,11 +63,6 @@ type ChannelsMessagesCmd struct { } func (c *ChannelsMessagesCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "teamId": c.TeamID, "channelId": c.ChannelID, @@ -100,20 +71,11 @@ func (c *ChannelsMessagesCmd) Run(ctx *commands.Context) error { args["top"] = c.Max } - resp, err := client.CallTool(ctx.Ctx, "ListChannelMessages", args) - if err != nil { - return fmt.Errorf("list channel messages: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "ListChannelMessages", "list channel messages", args) if err != nil { return err } - rows := output.ToRows(data, "messages") - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("messages", output.MessagesColumns, rows) + return ctx.Output.PrintListFromData("messages", output.MessagesColumns, data, c.Max) } // ChannelsPostCmd posts a message to a channel. @@ -136,21 +98,11 @@ func (c *ChannelsPostCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "PostChannelMessage", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "PostChannelMessage", "post channel message", map[string]any{ "teamId": c.TeamID, "channelId": c.ChannelID, "content": c.Message, }) - if err != nil { - return fmt.Errorf("post channel message: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -179,22 +131,12 @@ func (c *ChannelsReplyCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ReplyToChannelMessage", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "ReplyToChannelMessage", "reply to channel message", map[string]any{ "teamId": c.TeamID, "channelId": c.ChannelID, "messageId": c.MessageID, "content": c.Message, }) - if err != nil { - return fmt.Errorf("reply to channel message: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -223,11 +165,6 @@ func (c *ChannelsCreateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "teamId": c.TeamID, "displayName": c.DisplayName, @@ -236,12 +173,7 @@ func (c *ChannelsCreateCmd) Run(ctx *commands.Context) error { args["description"] = c.Description } - resp, err := client.CallTool(ctx.Ctx, "CreateChannel", args) - if err != nil { - return fmt.Errorf("create channel: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "CreateChannel", "create channel", args) if err != nil { return err } @@ -268,11 +200,6 @@ func (c *ChannelsCreatePrivateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "teamId": c.TeamID, "displayName": c.DisplayName, @@ -281,12 +208,7 @@ func (c *ChannelsCreatePrivateCmd) Run(ctx *commands.Context) error { args["description"] = c.Description } - resp, err := client.CallTool(ctx.Ctx, "CreatePrivateChannel", args) - if err != nil { - return fmt.Errorf("create private channel: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "CreatePrivateChannel", "create private channel", args) if err != nil { return err } @@ -315,11 +237,6 @@ func (c *ChannelsUpdateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "teamId": c.TeamID, "channelId": c.ChannelID, @@ -331,12 +248,7 @@ func (c *ChannelsUpdateCmd) Run(ctx *commands.Context) error { args["description"] = c.Description } - resp, err := client.CallTool(ctx.Ctx, "UpdateChannel", args) - if err != nil { - return fmt.Errorf("update channel: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "UpdateChannel", "update channel", args) if err != nil { return err } @@ -353,11 +265,6 @@ type ChannelsListMembersCmd struct { } func (c *ChannelsListMembersCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "teamId": c.TeamID, "channelId": c.ChannelID, @@ -366,23 +273,11 @@ func (c *ChannelsListMembersCmd) Run(ctx *commands.Context) error { args["top"] = c.Max } - resp, err := client.CallTool(ctx.Ctx, "ListChannelMembers", args) - if err != nil { - return fmt.Errorf("list channel members: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "ListChannelMembers", "list channel members", args) if err != nil { return err } - rows := output.ToRows(data, "members") - if rows == nil { - rows = output.ToRows(data, "value") - } - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("members", output.MembersColumns, rows) + return ctx.Output.PrintListFromData("members", output.MembersColumns, data, c.Max, "members", "value") } // ChannelsAddMemberCmd adds a member to a channel. @@ -405,21 +300,11 @@ func (c *ChannelsAddMemberCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "AddChannelMember", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "AddChannelMember", "add channel member", map[string]any{ "teamId": c.TeamID, "channelId": c.ChannelID, "userId": c.UserID, }) - if err != nil { - return fmt.Errorf("add channel member: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -448,22 +333,12 @@ func (c *ChannelsUpdateMemberCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateChannelMember", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "UpdateChannelMember", "update channel member", map[string]any{ "teamId": c.TeamID, "channelId": c.ChannelID, "membershipId": c.MembershipID, "role": c.Role, }) - if err != nil { - return fmt.Errorf("update channel member: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/teams/chats.go b/internal/commands/teams/chats.go index cb12bdc..4817a0a 100644 --- a/internal/commands/teams/chats.go +++ b/internal/commands/teams/chats.go @@ -32,11 +32,6 @@ type ChatsListCmd struct { } func (c *ChatsListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{} if len(c.UPNs) > 0 { args["userUpns"] = c.UPNs @@ -52,20 +47,11 @@ func (c *ChatsListCmd) Run(ctx *commands.Context) error { args["top"] = c.Max } - resp, err := client.CallTool(ctx.Ctx, "ListChats", args) - if err != nil { - return fmt.Errorf("list chats: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "ListChats", "list chats", args) if err != nil { return err } - rows := output.ToRows(data, "chats") - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("chats", output.ChatsColumns, rows) + return ctx.Output.PrintListFromData("chats", output.ChatsColumns, data, c.Max) } // ChatsGetCmd gets a specific chat. @@ -74,19 +60,9 @@ type ChatsGetCmd struct { } func (c *ChatsGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetChat", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "GetChat", "get chat", map[string]any{ "chatId": c.ChatID, }) - if err != nil { - return fmt.Errorf("get chat: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -100,27 +76,13 @@ type ChatsMessagesCmd struct { } func (c *ChatsMessagesCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ListChatMessages", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "ListChatMessages", "list chat messages", map[string]any{ "chatId": c.ChatID, }) - if err != nil { - return fmt.Errorf("list chat messages: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } - rows := output.ToRows(data, "messages") - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("messages", output.MessagesColumns, rows) + return ctx.Output.PrintListFromData("messages", output.MessagesColumns, data, c.Max) } // ChatsSendCmd sends a message to a chat. @@ -141,20 +103,10 @@ func (c *ChatsSendCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "PostMessage", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "PostMessage", "send message", map[string]any{ "chatId": c.ChatID, "content": c.Message, }) - if err != nil { - return fmt.Errorf("send message: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -174,19 +126,9 @@ func (c *ChatsSendSelfCmd) Run(ctx *commands.Context) error { }) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "SendMessageToSelf", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "SendMessageToSelf", "send to self", map[string]any{ "content": c.Message, }) - if err != nil { - return fmt.Errorf("send to self: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -200,20 +142,10 @@ type ChatsGetMessageCmd struct { } func (c *ChatsGetMessageCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetChatMessage", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "GetChatMessage", "get message", map[string]any{ "chatId": c.ChatID, "messageId": c.MessageID, }) - if err != nil { - return fmt.Errorf("get message: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -242,11 +174,6 @@ func (c *ChatsCreateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "chatType": c.Type, "members_upns": c.Members, @@ -255,12 +182,7 @@ func (c *ChatsCreateCmd) Run(ctx *commands.Context) error { args["topic"] = c.Topic } - resp, err := client.CallTool(ctx.Ctx, "CreateChat", args) - if err != nil { - return fmt.Errorf("create chat: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "CreateChat", "create chat", args) if err != nil { return err } @@ -284,19 +206,9 @@ func (c *ChatsDeleteCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "DeleteChat", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "DeleteChat", "delete chat", map[string]any{ "chatId": c.ChatID, }) - if err != nil { - return fmt.Errorf("delete chat: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -317,20 +229,10 @@ func (c *ChatsUpdateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateChat", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "UpdateChat", "update chat", map[string]any{ "chatId": c.ChatID, "topic": c.Topic, }) - if err != nil { - return fmt.Errorf("update chat: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -359,21 +261,11 @@ func (c *ChatsUpdateMessageCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "UpdateChatMessage", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "UpdateChatMessage", "update message", map[string]any{ "chatId": c.ChatID, "messageId": c.MessageID, "content": c.Content, }) - if err != nil { - return fmt.Errorf("update message: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -398,20 +290,10 @@ func (c *ChatsDeleteMessageCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "DeleteChatMessage", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "DeleteChatMessage", "delete message", map[string]any{ "chatId": c.ChatID, "messageId": c.MessageID, }) - if err != nil { - return fmt.Errorf("delete message: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -426,27 +308,13 @@ type ChatsListMembersCmd struct { } func (c *ChatsListMembersCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ListChatMembers", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "ListChatMembers", "list chat members", map[string]any{ "chatId": c.ChatID, }) - if err != nil { - return fmt.Errorf("list chat members: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } - rows := output.ToRows(data, "members") - if rows == nil { - rows = output.ToRows(data, "value") - } - return ctx.Output.PrintList("members", output.MembersColumns, rows) + return ctx.Output.PrintListFromData("members", output.MembersColumns, data, 0, "members", "value") } // ChatsAddMemberCmd adds a member to a chat. @@ -471,22 +339,12 @@ func (c *ChatsAddMemberCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "AddChatMember", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "AddChatMember", "add chat member", map[string]any{ "chatId": c.ChatID, "roles": c.Roles, "userodata_bind": fmt.Sprintf("https://graph.microsoft.com/v1.0/users('%s')", c.UPN), "odata_type": "#microsoft.graph.aadUserConversationMember", }) - if err != nil { - return fmt.Errorf("add chat member: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/teams/search.go b/internal/commands/teams/search.go index 4431f9a..0aea24a 100644 --- a/internal/commands/teams/search.go +++ b/internal/commands/teams/search.go @@ -1,8 +1,6 @@ package teams import ( - "fmt" - "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/output" ) @@ -14,11 +12,6 @@ type SearchCmd struct { } func (c *SearchCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "queryString": c.Query, } @@ -26,12 +19,7 @@ func (c *SearchCmd) Run(ctx *commands.Context) error { args["size"] = c.Size } - resp, err := client.CallTool(ctx.Ctx, "SearchTeamMessagesQueryParameters", args) - if err != nil { - return fmt.Errorf("search: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "SearchTeamMessagesQueryParameters", "search", args) if err != nil { return err } @@ -97,11 +85,6 @@ type SearchNLCmd struct { } func (c *SearchNLCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "message": c.Query, } @@ -109,12 +92,7 @@ func (c *SearchNLCmd) Run(ctx *commands.Context) error { args["conversationId"] = c.ConversationID } - resp, err := client.CallTool(ctx.Ctx, "SearchTeamsMessages", args) - if err != nil { - return fmt.Errorf("search: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "SearchTeamsMessages", "search", args) if err != nil { return err } diff --git a/internal/commands/teams/teams.go b/internal/commands/teams/teams.go index 1bcee4e..101c10a 100644 --- a/internal/commands/teams/teams.go +++ b/internal/commands/teams/teams.go @@ -1,8 +1,6 @@ package teams import ( - "fmt" - "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" "github.com/sozercan/a365cli/internal/output" @@ -10,12 +8,12 @@ import ( // TeamsCmd groups all Teams subcommands. type TeamsCmd struct { - List TeamsListCmd `cmd:"" help:"List joined teams"` - Get TeamsGetCmd `cmd:"" help:"Get a team by ID"` - Channels ChannelsCmd `cmd:"" help:"Team channels"` - Chats ChatsCmd `cmd:"" help:"Team chats"` - Search SearchCmd `cmd:"" help:"Search Teams messages (KQL)"` - SearchNL SearchNLCmd `cmd:"" name:"search-nl" help:"Search Teams messages (natural language)"` + List TeamsListCmd `cmd:"" help:"List joined teams"` + Get TeamsGetCmd `cmd:"" help:"Get a team by ID"` + Channels ChannelsCmd `cmd:"" help:"Team channels"` + Chats ChatsCmd `cmd:"" help:"Team chats"` + Search SearchCmd `cmd:"" help:"Search Teams messages (KQL)"` + SearchNL SearchNLCmd `cmd:"" name:"search-nl" help:"Search Teams messages (natural language)"` } // teamsEndpoint returns the agent365 endpoint for the Teams MCP server. @@ -34,11 +32,6 @@ type TeamsListCmd struct { } func (c *TeamsListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{} if c.UserID != "" { args["userId"] = c.UserID @@ -46,20 +39,11 @@ func (c *TeamsListCmd) Run(ctx *commands.Context) error { args["userId"] = ctx.UserUPN } - resp, err := client.CallTool(ctx.Ctx, "ListTeams", args) - if err != nil { - return fmt.Errorf("list teams: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(teamsEndpoint(), "ListTeams", "list teams", args) if err != nil { return err } - rows := output.ToRows(data, "teams") - if c.Max > 0 && len(rows) > c.Max { - rows = rows[:c.Max] - } - return ctx.Output.PrintList("teams", output.TeamsColumns, rows) + return ctx.Output.PrintListFromData("teams", output.TeamsColumns, data, c.Max) } // TeamsGetCmd gets a team by ID. @@ -68,19 +52,9 @@ type TeamsGetCmd struct { } func (c *TeamsGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(teamsEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetTeam", map[string]any{ + data, err := ctx.CallToolData(teamsEndpoint(), "GetTeam", "get team", map[string]any{ "teamId": c.ID, }) - if err != nil { - return fmt.Errorf("get team: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/triggers/triggers.go b/internal/commands/triggers/triggers.go index 33e0bd0..4341a02 100644 --- a/internal/commands/triggers/triggers.go +++ b/internal/commands/triggers/triggers.go @@ -5,7 +5,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // TriggersCmd groups all Triggers subcommands. @@ -31,17 +30,7 @@ func triggersEndpoint() string { type TriggersEventsCmd struct{} func (c *TriggersEventsCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "list_event_types", map[string]any{}) - if err != nil { - return fmt.Errorf("list event types: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(triggersEndpoint(), "list_event_types", "list event types", map[string]any{}) if err != nil { return err } @@ -56,19 +45,9 @@ type TriggersSchemaCmd struct { } func (c *TriggersSchemaCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "get_event_type_schema", map[string]any{ + data, err := ctx.CallToolData(triggersEndpoint(), "get_event_type_schema", "get event type schema", map[string]any{ "eventType": c.EventType, }) - if err != nil { - return fmt.Errorf("get event type schema: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -83,19 +62,9 @@ type TriggersValidateCmd struct { } func (c *TriggersValidateCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "validate_trigger", map[string]any{ + data, err := ctx.CallToolData(triggersEndpoint(), "validate_trigger", "validate trigger", map[string]any{ "userRequest": c.UserRequest, }) - if err != nil { - return fmt.Errorf("validate trigger: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -134,12 +103,7 @@ func (c *TriggersCreateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "create_trigger_definition", map[string]any{ + data, err := ctx.CallToolData(triggersEndpoint(), "create_trigger_definition", "create trigger", map[string]any{ "validationToken": c.ValidationToken, "name": c.Name, "eventType": c.EventType, @@ -147,11 +111,6 @@ func (c *TriggersCreateCmd) Run(ctx *commands.Context) error { "conditions": c.Conditions, "instructions": c.Instructions, }) - if err != nil { - return fmt.Errorf("create trigger: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -164,17 +123,7 @@ func (c *TriggersCreateCmd) Run(ctx *commands.Context) error { type TriggersListCmd struct{} func (c *TriggersListCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "list_trigger_definitions", map[string]any{}) - if err != nil { - return fmt.Errorf("list triggers: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(triggersEndpoint(), "list_trigger_definitions", "list triggers", map[string]any{}) if err != nil { return err } @@ -189,19 +138,9 @@ type TriggersGetCmd struct { } func (c *TriggersGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "get_trigger_definition", map[string]any{ + data, err := ctx.CallToolData(triggersEndpoint(), "get_trigger_definition", "get trigger", map[string]any{ "id": c.ID, }) - if err != nil { - return fmt.Errorf("get trigger: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -231,20 +170,10 @@ func (c *TriggersUpdateCmd) Run(ctx *commands.Context) error { ) } - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "update_trigger_definition", map[string]any{ + data, err := ctx.CallToolData(triggersEndpoint(), "update_trigger_definition", "update trigger", map[string]any{ "validationToken": c.ValidationToken, "id": c.ID, }) - if err != nil { - return fmt.Errorf("update trigger: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -271,19 +200,9 @@ func (c *TriggersDeleteCmd) Run(ctx *commands.Context) error { return err } - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "delete_trigger_definition", map[string]any{ + data, err := ctx.CallToolData(triggersEndpoint(), "delete_trigger_definition", "delete trigger", map[string]any{ "id": c.ID, }) - if err != nil { - return fmt.Errorf("delete trigger: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -299,20 +218,10 @@ type TriggersEvaluateCmd struct { } func (c *TriggersEvaluateCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(triggersEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "evaluate_event_triggers", map[string]any{ + data, err := ctx.CallToolData(triggersEndpoint(), "evaluate_event_triggers", "evaluate triggers", map[string]any{ "eventType": c.EventType, "eventDataJson": c.EventDataJSON, }) - if err != nil { - return fmt.Errorf("evaluate triggers: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/websearch/websearch.go b/internal/commands/websearch/websearch.go index 5803e59..96b4cc6 100644 --- a/internal/commands/websearch/websearch.go +++ b/internal/commands/websearch/websearch.go @@ -1,11 +1,8 @@ package websearch import ( - "fmt" - "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // WebSearchCmd groups Web Search subcommands. @@ -24,22 +21,12 @@ type WebSearchSearchCmd struct { } func (c *WebSearchSearchCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(websearchEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - args := map[string]any{ "query": c.Query, "urls": c.URLs, } - resp, err := client.CallTool(ctx.Ctx, "SearchWeb", args) - if err != nil { - return fmt.Errorf("search web: %w", err) - } - - data, err := output.ExtractContent(resp) + data, err := ctx.CallToolData(websearchEndpoint(), "SearchWeb", "search web", args) if err != nil { return err } diff --git a/internal/commands/word/word.go b/internal/commands/word/word.go index eb6080a..d8690e5 100644 --- a/internal/commands/word/word.go +++ b/internal/commands/word/word.go @@ -5,7 +5,6 @@ import ( "github.com/sozercan/a365cli/internal/commands" "github.com/sozercan/a365cli/internal/config" - "github.com/sozercan/a365cli/internal/output" ) // WordCmd groups all Word subcommands. @@ -22,30 +21,26 @@ func wordEndpoint() string { // WordCreateCmd creates a new Word document. type WordCreateCmd struct { - FileName string `arg:"" help:"Desired file name for the new document"` + FileName string `arg:"" help:"Desired file name for the new document"` + ContentInHTML string `help:"HTML or plain text content for the document body" name:"content" optional:"" default:""` } func (c *WordCreateCmd) Run(ctx *commands.Context) error { if ctx.DryRun { return ctx.ValidateDryRun(wordEndpoint(), "CreateDocument", fmt.Sprintf("create Word document %q", c.FileName), - map[string]any{"action": "word.create", "desiredFileName": c.FileName}, + map[string]any{ + "action": "word.create", + "fileName": c.FileName, + "contentInHtml": c.ContentInHTML, + }, ) } - client := ctx.NewMCPClient(wordEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "CreateDocument", map[string]any{ - "desiredFileName": c.FileName, + data, err := ctx.CallToolData(wordEndpoint(), "CreateDocument", "create document", map[string]any{ + "fileName": c.FileName, + "contentInHtml": c.ContentInHTML, }) - if err != nil { - return fmt.Errorf("create document: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -54,25 +49,13 @@ func (c *WordCreateCmd) Run(ctx *commands.Context) error { // WordGetCmd gets Word document content. type WordGetCmd struct { - DriveID string `arg:"" help:"Drive ID"` - DocumentID string `arg:"" help:"Document ID"` + URL string `arg:"" help:"SharePoint sharing URL for the document"` } func (c *WordGetCmd) Run(ctx *commands.Context) error { - client := ctx.NewMCPClient(wordEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "GetDocumentContent", map[string]any{ - "driveId": c.DriveID, - "documentId": c.DocumentID, + data, err := ctx.CallToolData(wordEndpoint(), "GetDocumentContent", "get document content", map[string]any{ + "url": c.URL, }) - if err != nil { - return fmt.Errorf("get document content: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -90,25 +73,20 @@ func (c *WordCommentCmd) Run(ctx *commands.Context) error { if ctx.DryRun { return ctx.ValidateDryRun(wordEndpoint(), "AddComment", fmt.Sprintf("add comment to document %s", c.DocumentID), - map[string]any{"action": "word.comment", "driveId": c.DriveID, "documentId": c.DocumentID}, + map[string]any{ + "action": "word.comment", + "driveId": c.DriveID, + "documentId": c.DocumentID, + "newComment": c.Text, + }, ) } - client := ctx.NewMCPClient(wordEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "AddComment", map[string]any{ + data, err := ctx.CallToolData(wordEndpoint(), "AddComment", "add comment", map[string]any{ "driveId": c.DriveID, "documentId": c.DocumentID, - "text": c.Text, + "newComment": c.Text, }) - if err != nil { - return fmt.Errorf("add comment: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } @@ -127,26 +105,22 @@ func (c *WordReplyCmd) Run(ctx *commands.Context) error { if ctx.DryRun { return ctx.ValidateDryRun(wordEndpoint(), "ReplyToComment", fmt.Sprintf("reply to comment %s on document %s", c.CommentID, c.DocumentID), - map[string]any{"action": "word.reply", "commentId": c.CommentID, "driveId": c.DriveID, "documentId": c.DocumentID}, + map[string]any{ + "action": "word.reply", + "commentId": c.CommentID, + "driveId": c.DriveID, + "documentId": c.DocumentID, + "newComment": c.Text, + }, ) } - client := ctx.NewMCPClient(wordEndpoint()) - if err := client.Initialize(ctx.Ctx); err != nil { - return fmt.Errorf("initialize: %w", err) - } - - resp, err := client.CallTool(ctx.Ctx, "ReplyToComment", map[string]any{ + data, err := ctx.CallToolData(wordEndpoint(), "ReplyToComment", "reply to comment", map[string]any{ "commentId": c.CommentID, "driveId": c.DriveID, "documentId": c.DocumentID, - "text": c.Text, + "newComment": c.Text, }) - if err != nil { - return fmt.Errorf("reply to comment: %w", err) - } - - data, err := output.ExtractContent(resp) if err != nil { return err } diff --git a/internal/commands/word/word_test.go b/internal/commands/word/word_test.go index 12acf88..c6b0237 100644 --- a/internal/commands/word/word_test.go +++ b/internal/commands/word/word_test.go @@ -15,17 +15,18 @@ func TestWordCreateCmd_DryRun(t *testing.T) { InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ - "action": map[string]any{"type": "string"}, - "desiredFileName": map[string]any{"type": "string"}, + "action": map[string]any{"type": "string"}, + "fileName": map[string]any{"type": "string"}, + "contentInHtml": map[string]any{"type": "string"}, }, - "required": []any{"desiredFileName"}, + "required": []any{"fileName", "contentInHtml"}, }, }, } ctx, buf := testutil.SetupTestServerWithSchemas(t, nil, schemas) ctx.DryRun = true - cmd := &WordCreateCmd{FileName: "report.docx"} + cmd := &WordCreateCmd{FileName: "report.docx", ContentInHTML: "

Status update

"} if err := cmd.Run(ctx); err != nil { t.Fatalf("Run() error: %v", err) } @@ -56,8 +57,9 @@ func TestWordCommentCmd_DryRun(t *testing.T) { "action": map[string]any{"type": "string"}, "driveId": map[string]any{"type": "string"}, "documentId": map[string]any{"type": "string"}, + "newComment": map[string]any{"type": "string"}, }, - "required": []any{"driveId", "documentId"}, + "required": []any{"driveId", "documentId", "newComment"}, }, }, } diff --git a/internal/output/formatter.go b/internal/output/formatter.go index 20d162c..7bccd3a 100644 --- a/internal/output/formatter.go +++ b/internal/output/formatter.go @@ -54,6 +54,42 @@ func (f *Formatter) PrintList(entity string, columns []Column, rows []map[string } } +// PrintListFromData extracts the first list found under keys, applies an optional +// max limit, and prints it with the given entity name and column definitions. +// If none of the keys contain a list, it falls back to printing the full item so +// callers keep useful output when a server returns an unexpected shape. +func (f *Formatter) PrintListFromData(entity string, columns []Column, data map[string]any, max int, keys ...string) error { + rows := RowsFromData(data, max, appendDefaultKey(entity, keys)...) + if rows == nil { + return f.PrintItem(data) + } + return f.PrintList(entity, columns, rows) +} + +// RowsFromData extracts the first list found under keys and applies an optional +// max limit. A missing or non-list key returns nil; a present empty list returns +// an empty slice. +func RowsFromData(data map[string]any, max int, keys ...string) []map[string]any { + for _, key := range keys { + rows := ToRows(data, key) + if rows == nil { + continue + } + if max > 0 && len(rows) > max { + return rows[:max] + } + return rows + } + return nil +} + +func appendDefaultKey(entity string, keys []string) []string { + if len(keys) > 0 { + return keys + } + return []string{entity} +} + // PrintItem outputs a single item. func (f *Formatter) PrintItem(item map[string]any) error { switch f.Format { diff --git a/internal/output/formatter_test.go b/internal/output/formatter_test.go index 433bbda..b835e7f 100644 --- a/internal/output/formatter_test.go +++ b/internal/output/formatter_test.go @@ -478,3 +478,70 @@ func TestPrintList_EmptyRows(t *testing.T) { }) } } + +func TestRowsFromData_FirstMatchingKeyAndMax(t *testing.T) { + data := map[string]any{ + "value": []any{ + map[string]any{"name": "one"}, + map[string]any{"name": "two"}, + }, + } + + rows := RowsFromData(data, 1, "missing", "value") + if len(rows) != 1 { + t.Fatalf("expected one row after max trim, got %d", len(rows)) + } + if rows[0]["name"] != "one" { + t.Fatalf("expected first row, got %#v", rows[0]) + } +} + +func TestRowsFromData_MissingListReturnsNil(t *testing.T) { + rows := RowsFromData(map[string]any{"message": "ok"}, 0, "value") + if rows != nil { + t.Fatalf("expected nil rows for missing list, got %#v", rows) + } +} + +func TestPrintListFromData_JSON(t *testing.T) { + var buf bytes.Buffer + f := &Formatter{Format: FormatJSON, Writer: &buf} + columns := []Column{{Header: "NAME", Extract: func(r map[string]any) string { return getString(r, "name") }}} + data := map[string]any{ + "value": []any{ + map[string]any{"name": "one"}, + map[string]any{"name": "two"}, + }, + } + + if err := f.PrintListFromData("items", columns, data, 1, "items", "value"); err != nil { + t.Fatalf("PrintListFromData failed: %v", err) + } + + var parsed map[string]any + if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil { + t.Fatalf("output is not valid JSON: %v", err) + } + items, ok := parsed["items"].([]any) + if !ok || len(items) != 1 { + t.Fatalf("expected one item, got %#v", parsed["items"]) + } +} + +func TestPrintListFromData_FallsBackToItem(t *testing.T) { + var buf bytes.Buffer + f := &Formatter{Format: FormatJSON, Writer: &buf} + columns := []Column{{Header: "NAME", Extract: func(r map[string]any) string { return getString(r, "name") }}} + + if err := f.PrintListFromData("items", columns, map[string]any{"message": "ok"}, 0, "items"); err != nil { + t.Fatalf("PrintListFromData failed: %v", err) + } + + var parsed map[string]any + if err := json.Unmarshal(buf.Bytes(), &parsed); err != nil { + t.Fatalf("output is not valid JSON: %v", err) + } + if parsed["message"] != "ok" { + t.Fatalf("expected fallback item, got %#v", parsed) + } +}