diff --git a/cmd/admin/org_contacts.go b/cmd/admin/org_contacts.go index e87ff06..67b52bd 100644 --- a/cmd/admin/org_contacts.go +++ b/cmd/admin/org_contacts.go @@ -28,7 +28,7 @@ func init() { cmd.AdminCmd.AddCommand(orgContactsCmd) { // create - var orgId string + var orgid string var bodyRaw string var bodyFile string cmd := &cobra.Command{ @@ -36,8 +36,8 @@ func init() { Short: "Create a Contact", Long: "Creating a new contact for a given organization requires an org admin role.\n\nAt least one of the following body parameters: `phoneNumbers`, `emails`, `sipAddresses` is required to create a new contact for source \"CH\",\n`displayName` is required to create a new contact for source \"Webex4Broadworks\".\n\nUse the optional `groupIds` field to add group IDs in an array within the organisation contact. This will become a group contact.", RunE: func(cmd *cobra.Command, args []string) error { - req := client.NewRequest(config.CallingBaseURL, "POST", "/contacts/organizations/{orgId}/contacts") - req.PathParam("orgId", orgId) + req := client.NewRequest(config.CallingBaseURL, "POST", "/contacts/organizations/{orgid}/contacts") + req.PathParam("orgid", orgid) if bodyFile != "" { if err := req.SetBodyFile(bodyFile); err != nil { return err @@ -52,23 +52,23 @@ func init() { return output.Print(resp, statusCode) }, } - cmd.Flags().StringVar(&orgId, "org-id", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") - cmd.MarkFlagRequired("org-id") + cmd.Flags().StringVar(&orgid, "orgid", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") + cmd.MarkFlagRequired("orgid") cmd.Flags().StringVar(&bodyRaw, "body", "", "Raw JSON body") cmd.Flags().StringVar(&bodyFile, "body-file", "", "Path to JSON body file") orgContactsCmd.AddCommand(cmd) } { // get - var orgId string + var orgid string var contactId string cmd := &cobra.Command{ Use: "get", Short: "Get a Contact", Long: "Shows details for an organization contact by ID.\nSpecify the organization ID in the `orgId` parameter in the URI, and specify the contact ID in the `contactId` parameter in the URI.\n\n**NOTE**:\nThe `orgId` used in the path for this API are the org UUIDs. They follow a xxxx-xxxx-xxxx-xxxx pattern. If you have an orgId in base64 encoded format (starting with Y2.....) you need to base64 decode the id and extract the UUID from the slug, before you use it in your API call.", RunE: func(cmd *cobra.Command, args []string) error { - req := client.NewRequest(config.CallingBaseURL, "GET", "/contacts/organizations/{orgId}/contacts/{contactId}") - req.PathParam("orgId", orgId) + req := client.NewRequest(config.CallingBaseURL, "GET", "/contacts/organizations/{orgid}/contacts/{contactId}") + req.PathParam("orgid", orgid) req.PathParam("contactId", contactId) if config.Paginate() { resp, statusCode, err := req.DoPaginated(true) @@ -84,15 +84,15 @@ func init() { return output.Print(resp, statusCode) }, } - cmd.Flags().StringVar(&orgId, "org-id", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") - cmd.MarkFlagRequired("org-id") + cmd.Flags().StringVar(&orgid, "orgid", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") + cmd.MarkFlagRequired("orgid") cmd.Flags().StringVar(&contactId, "contact-id", "", "The contact ID.") cmd.MarkFlagRequired("contact-id") orgContactsCmd.AddCommand(cmd) } { // update - var orgId string + var orgid string var contactId string var bodyRaw string var bodyFile string @@ -101,8 +101,8 @@ func init() { Short: "Update a Contact", Long: "Update details for contact by ID. Only an admin can update a contact.\nSpecify the organization ID in the `orgId` parameter in the URI, and specify the contact ID in the `contactId` parameter in the URI.\n\nUse the optional `groupIds` field to update the group IDs by changing the existing array. You can add or remove one or all groups. To remove all associated groups, pass an empty array in the `groupIds` field.", RunE: func(cmd *cobra.Command, args []string) error { - req := client.NewRequest(config.CallingBaseURL, "PATCH", "/contacts/organizations/{orgId}/contacts/{contactId}") - req.PathParam("orgId", orgId) + req := client.NewRequest(config.CallingBaseURL, "PATCH", "/contacts/organizations/{orgid}/contacts/{contactId}") + req.PathParam("orgid", orgid) req.PathParam("contactId", contactId) if bodyFile != "" { if err := req.SetBodyFile(bodyFile); err != nil { @@ -118,8 +118,8 @@ func init() { return output.Print(resp, statusCode) }, } - cmd.Flags().StringVar(&orgId, "org-id", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") - cmd.MarkFlagRequired("org-id") + cmd.Flags().StringVar(&orgid, "orgid", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") + cmd.MarkFlagRequired("orgid") cmd.Flags().StringVar(&contactId, "contact-id", "", "The contact ID.") cmd.MarkFlagRequired("contact-id") cmd.Flags().StringVar(&bodyRaw, "body", "", "Raw JSON body") @@ -128,15 +128,15 @@ func init() { } { // delete - var orgId string + var orgid string var contactId string cmd := &cobra.Command{ Use: "delete", Short: "Delete a Contact", Long: "Remove a contact from the organization. Only an admin can remove a contact.\n\nSpecify the organization ID in the `orgId` parameter in the URI, and specify the contact ID in the `contactId` parameter in the URI.", RunE: func(cmd *cobra.Command, args []string) error { - req := client.NewRequest(config.CallingBaseURL, "DELETE", "/contacts/organizations/{orgId}/contacts/{contactId}") - req.PathParam("orgId", orgId) + req := client.NewRequest(config.CallingBaseURL, "DELETE", "/contacts/organizations/{orgid}/contacts/{contactId}") + req.PathParam("orgid", orgid) req.PathParam("contactId", contactId) resp, statusCode, err := req.Do() if err != nil { @@ -145,15 +145,15 @@ func init() { return output.Print(resp, statusCode) }, } - cmd.Flags().StringVar(&orgId, "org-id", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") - cmd.MarkFlagRequired("org-id") + cmd.Flags().StringVar(&orgid, "orgid", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") + cmd.MarkFlagRequired("orgid") cmd.Flags().StringVar(&contactId, "contact-id", "", "The contact ID.") cmd.MarkFlagRequired("contact-id") orgContactsCmd.AddCommand(cmd) } { // list - var orgId string + var orgid string var keyword string var source string var limit string @@ -163,8 +163,8 @@ func init() { Short: "List Contacts", Long: "List contacts in the organization. The default limit is `100`.\n\n`keyword` can be the value of \"displayName\", \"firstName\", \"lastName\", \"email\". An empty string of `keyword` means get all contacts.\n\n`groupIds` is a comma separated list group IDs. Results are filtered based on those group IDs.\n\nLong result sets will be split into [pages](/docs/basics#pagination).", RunE: func(cmd *cobra.Command, args []string) error { - req := client.NewRequest(config.CallingBaseURL, "GET", "/contacts/organizations/{orgId}/contacts/search") - req.PathParam("orgId", orgId) + req := client.NewRequest(config.CallingBaseURL, "GET", "/contacts/organizations/{orgid}/contacts/search") + req.PathParam("orgid", orgid) req.QueryParam("keyword", keyword) req.QueryParam("source", source) req.QueryParam("limit", limit) @@ -184,8 +184,8 @@ func init() { return output.Print(resp, statusCode) }, } - cmd.Flags().StringVar(&orgId, "org-id", "", "The organization ID.") - cmd.MarkFlagRequired("org-id") + cmd.Flags().StringVar(&orgid, "orgid", "", "The organization ID.") + cmd.MarkFlagRequired("orgid") cmd.Flags().StringVar(&keyword, "keyword", "", "List contacts with a keyword.") cmd.Flags().StringVar(&source, "source", "", "List contacts with source.") cmd.Flags().StringVar(&limit, "limit", "", "Limit the maximum number of contact in the response. + Default: 100 ") @@ -194,7 +194,7 @@ func init() { } { // bulk-create-update - var orgId string + var orgid string var bodyRaw string var bodyFile string cmd := &cobra.Command{ @@ -202,8 +202,8 @@ func init() { Short: "Bulk Create or Update Contacts", Long: "Create or update contacts in bulk. Update an existing contact by specifying the contact ID in the `contactId` parameter in the request body.", RunE: func(cmd *cobra.Command, args []string) error { - req := client.NewRequest(config.CallingBaseURL, "POST", "/contacts/organizations/{orgId}/contacts/bulk") - req.PathParam("orgId", orgId) + req := client.NewRequest(config.CallingBaseURL, "POST", "/contacts/organizations/{orgid}/contacts/bulk") + req.PathParam("orgid", orgid) if bodyFile != "" { if err := req.SetBodyFile(bodyFile); err != nil { return err @@ -218,15 +218,15 @@ func init() { return output.Print(resp, statusCode) }, } - cmd.Flags().StringVar(&orgId, "org-id", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") - cmd.MarkFlagRequired("org-id") + cmd.Flags().StringVar(&orgid, "orgid", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") + cmd.MarkFlagRequired("orgid") cmd.Flags().StringVar(&bodyRaw, "body", "", "Raw JSON body") cmd.Flags().StringVar(&bodyFile, "body-file", "", "Path to JSON body file") orgContactsCmd.AddCommand(cmd) } { // bulk-delete - var orgId string + var orgid string var schemas string var objectIds []string var bodyRaw string @@ -236,8 +236,8 @@ func init() { Short: "Bulk Delete Contacts", Long: `Delete contacts in bulk.`, RunE: func(cmd *cobra.Command, args []string) error { - req := client.NewRequest(config.CallingBaseURL, "POST", "/contacts/organizations/{orgId}/contacts/bulk/delete") - req.PathParam("orgId", orgId) + req := client.NewRequest(config.CallingBaseURL, "POST", "/contacts/organizations/{orgid}/contacts/bulk/delete") + req.PathParam("orgid", orgid) if bodyFile != "" { if err := req.SetBodyFile(bodyFile); err != nil { return err @@ -255,8 +255,8 @@ func init() { return output.Print(resp, statusCode) }, } - cmd.Flags().StringVar(&orgId, "org-id", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") - cmd.MarkFlagRequired("org-id") + cmd.Flags().StringVar(&orgid, "orgid", "", "Webex Identity assigned organization identifier for the user's organization or the organization he manages.") + cmd.MarkFlagRequired("orgid") cmd.Flags().StringVar(&schemas, "schemas", "", "") cmd.Flags().StringSliceVar(&objectIds, "object-ids", nil, "") cmd.Flags().StringVar(&bodyRaw, "body", "", "Raw JSON body") diff --git a/codegen/generate_cli.py b/codegen/generate_cli.py index 4775d29..70eb8ad 100644 --- a/codegen/generate_cli.py +++ b/codegen/generate_cli.py @@ -222,11 +222,14 @@ def generate_command(ep, group_var, base_url_const, is_calling): original_name = ep.get('original_name', cmd_name) has_from = any(p['name'] == 'from' for p in query_params) - # Normalize orgId → orgid for CC commands so auto-populate in root.go works - # consistently (CC APIs use UUID format via the --orgid flag path). + # Normalize orgId → orgid so auto-populate in root.go resolves to UUID format. + # Applies to CC commands (is_calling=False) and to any endpoint under + # /contacts/organizations/ which explicitly requires UUID despite being in a + # non-CC collection (Postman note: "orgId used in path are the org UUIDs"). # For path params, rename both the flag and the param key (path uses {orgid}). # For query params, only rename the flag; keep the original API key (orgId). - if not is_calling: + needs_uuid_orgid = not is_calling or '/contacts/organizations/' in path + if needs_uuid_orgid: path = path.replace('{orgId}', '{orgid}') for p in path_params: if p['name'] == 'orgId': diff --git a/skill/SKILL.md b/skill/SKILL.md index d76e8f3..e19e410 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -76,6 +76,20 @@ webex cc site list --orgid="Y2lzY29zcGFyazovL3Vz..." # Base64 — auto-decoded If you need to override the org, use `--organization ` (global flag) which feeds into `--orgid` automatically. Both base64 and UUID formats are accepted. +## OrgID Format Troubleshooting + +**General rule:** CC commands (`webex cc ...`) use UUID format; all other commands (`webex admin`, `webex calling`, etc.) use base64 format. The CLI normalizes automatically, but if an API call returns a 400 or 404 with an org-related error, the format passed may be wrong for that endpoint. + +**If you get an org ID error, try the other format:** +```bash +# Got a 400 on a CC command? Make sure the value decodes to a plain UUID: +webex cc site list --orgid="4ebc486d-ff5f-4abc-9d44-1234567890ab" # UUID +# Got a 400 on an admin/calling command? Make sure it's base64: +webex admin people list --organization="Y2lzY29zcGFyazovL3Vz..." # Base64 +``` + +You can inspect what the CLI resolved to by running `webex auth status` — the org ID shown there is the base64 form. Strip the trailing `=` padding issues or swap formats if calls are failing with 400/404 on org-scoped endpoints. + ## CC Subcommand Names Most CC resources use a consistent `list` subcommand. A few exceptions remain: @@ -94,21 +108,83 @@ All other CC resources (site, team, users, global-variables, business-hour, audi 1. **Always redirect stderr separately** when capturing JSON output: ```bash - webex cc site list --orgid="$ORG" > /tmp/result.json 2>/tmp/error.log + # Mac/Linux/WSL — $TMPDIR is set on macOS; falls back to /tmp on Linux + webex cc site list --orgid="$ORG" > "${TMPDIR:-/tmp}/result.json" 2>"${TMPDIR:-/tmp}/error.log" + # Windows PowerShell + # webex cc site list --orgid="$ORG" > "$env:TEMP\result.json" 2> "$env:TEMP\error.log" ``` 2. **Use `--orgid=VALUE` syntax** (with `=`) for CC commands to avoid shell quoting issues. Do NOT use `--orgid "$VAR"` with a space — use `--orgid="$VAR"`. 3. **Write output to temp files first**, then read/analyze. Do NOT pipe webex output directly into python or jq in a single shell command — complex pipes can cause issues with the binary output. + - Mac/Linux/WSL: use `"${TMPDIR:-/tmp}/filename.json"` + - Windows PowerShell: use `"$env:TEMP\filename.json"` 4. **Check `--help` before guessing** subcommand names: ```bash webex --help ``` -## Admin API Examples +## Using as an MCP Server + +The CLI includes a built-in MCP server exposing four tools: + +| Tool | API methods | When to use | +|---|---|---| +| `webex_read` | GET | List, get, download, export — auto-approvable | +| `webex_write` | POST / PUT / PATCH / DELETE | Create, update, delete — prompts for confirmation | +| `webex_help` | — | Discover commands and flags | +| `webex_usage` | — | View recent MCP command history | + +**Claude Code / stdio clients** — no server process needed: +```bash +claude mcp add webex -- webex mcp serve +``` + +**Claude Desktop / HTTP clients** — start the server first, then point the config at it: +```bash +# Start server (loopback only, port 47890) +webex mcp serve --http +``` +Add to your Claude Desktop config file, then restart Claude Desktop: + +| Platform | Config path | +|---|---| +| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | +| Linux | `~/.config/Claude/claude_desktop_config.json` | +| Windows | `%APPDATA%\Claude\claude_desktop_config.json` | + +```json +{ + "mcpServers": { + "webex": { "url": "http://localhost:47890/mcp" } + } +} +``` +Use `--http-addr 127.0.0.1:` to change the port. +Only loopback addresses (127.x.x.x / ::1) are accepted — the server cannot bind to public interfaces. + +## Sub-Skills + +For detailed flags, body schemas, and usage examples, Read the sub-skill file before working in that area. + +Sub-skills are installed alongside this file. Resolve the base path for your platform: + +| Platform | Skills base path | +|---|---| +| macOS / Linux | `~/.claude/skills/webex-cli/` | +| Windows | `%USERPROFILE%\.claude\skills\webex-cli\` | + +| Area | Sub-path | +|---|---| +| Admin | `admin/SKILL.md` | +| Calling | `calling/SKILL.md` | +| Contact Center | `cc/SKILL.md` | +| Devices | `device/SKILL.md` | +| Meetings | `meetings/SKILL.md` | +| Messaging | `messaging/SKILL.md` | -Admin commands manage people, organizations, licenses, roles, and other administrative resources: +## Quick Reference ```bash # List people in the org @@ -386,8 +462,8 @@ webex admin people list --output=table # Raw JSON (no formatting) webex admin people list --output=raw -# Save to file for processing -webex admin people list > /tmp/people.json +# Save to file for processing (Mac/Linux/WSL) +webex admin people list > "${TMPDIR:-/tmp}/people.json" ``` ## Converged Recordings: Admin vs Non-Admin Endpoints