fix: respect page_size, slim list_clients output, add OAuth scope=all

Three fixes discovered during initial deployment against
interakt.halopsa.com:

- halo-client: include scope=all in the client_credentials token
  request. Without it Halo issues a token with no read scopes — every
  /api/* call fails authorization despite a successful exchange.
  Manifested as the original "permissions issue".

- tools: add pageinate=true to every list_* call. Halo ignores
  page_size unless pagination is explicitly enabled, so requesting
  page_size=5 was returning the server-side default of 50.

- tools: project list_clients to a summary shape (id, name, top-level,
  status, customer_relationship names). Full Halo client records are
  ~18 KB each; a 5-row page was 89 KB, exceeding tool-result budgets.
  Pass full=true for the original payload; get_client still returns
  full detail.

Also: switch list_clients filter from inactive=false to the canonical
includeinactive=false, and ship start.sh (the wrapper documented in
CLAUDE.md Step 4) so downstream setups don't have to author it.

Verified end-to-end against the live instance: GET /Client with
pageinate=true&page_size=3 now returns exactly 3 records and
record_count=122 (total active clients).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Piers Macrae Cockram 2026-04-27 02:28:16 +00:00
parent 57c191ee1f
commit f079a6aea8
3 changed files with 47 additions and 3 deletions

View File

@ -41,6 +41,7 @@ export class HaloClient {
grant_type: "client_credentials", grant_type: "client_credentials",
client_id: this.config.clientId, client_id: this.config.clientId,
client_secret: this.config.clientSecret, client_secret: this.config.clientSecret,
scope: "all",
}); });
if (this.config.tenant) { if (this.config.tenant) {

View File

@ -1,5 +1,24 @@
import { HaloClient } from "./halo-client"; import { HaloClient } from "./halo-client";
// ─── Helpers ─────────────────────────────────────────────────────────────────
function summarizeClient(c: any) {
return {
id: c.id,
name: c.name,
toplevel_id: c.toplevel_id,
toplevel_name: c.toplevel_name,
inactive: c.inactive,
is_vip: c.is_vip,
is_account: c.is_account,
accountsid: c.accountsid,
customertype: c.customertype,
customer_relationship: Array.isArray(c.customer_relationship)
? c.customer_relationship.map((r: any) => r.name).filter(Boolean)
: undefined,
};
}
// ─── Tool Definitions ──────────────────────────────────────────────────────── // ─── Tool Definitions ────────────────────────────────────────────────────────
export const TOOL_DEFINITIONS = [ export const TOOL_DEFINITIONS = [
@ -40,7 +59,7 @@ export const TOOL_DEFINITIONS = [
// ── Clients ── // ── Clients ──
{ {
name: "list_clients", name: "list_clients",
description: "List clients/customers with optional search. Returns company name, ID, status, and key info.", description: "List clients/customers with optional search. Returns a summary per client (id, name, top-level, status, customer relationships). For full detail use get_client.",
inputSchema: { inputSchema: {
type: "object" as const, type: "object" as const,
properties: { properties: {
@ -49,6 +68,7 @@ export const TOOL_DEFINITIONS = [
search: { type: "string", description: "Search by client name" }, search: { type: "string", description: "Search by client name" },
toplevel_id: { type: "number", description: "Filter by top-level/parent client ID" }, toplevel_id: { type: "number", description: "Filter by top-level/parent client ID" },
active_only: { type: "boolean", default: true }, active_only: { type: "boolean", default: true },
full: { type: "boolean", description: "Return the full Halo client payload instead of a summary", default: false },
}, },
}, },
}, },
@ -281,6 +301,7 @@ export async function handleTool(
// ── Tickets ── // ── Tickets ──
case "list_tickets": { case "list_tickets": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
order: "id_desc", order: "id_desc",
@ -305,13 +326,22 @@ export async function handleTool(
// ── Clients ── // ── Clients ──
case "list_clients": { case "list_clients": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };
if (args.search) params.search = args.search; if (args.search) params.search = args.search;
if (args.toplevel_id) params.toplevel_id = args.toplevel_id; if (args.toplevel_id) params.toplevel_id = args.toplevel_id;
if (args.active_only !== false) params.inactive = false; if (args.active_only !== false) params.includeinactive = false;
return client.get("/Client", params); const result = await client.get<any>("/Client", params);
if (args.full) return result;
const clients = Array.isArray(result?.clients) ? result.clients.map(summarizeClient) : [];
return {
page_no: result?.page_no,
page_size: result?.page_size,
record_count: result?.record_count,
clients,
};
} }
case "get_client": { case "get_client": {
@ -330,6 +360,7 @@ export async function handleTool(
// ── Contracts ── // ── Contracts ──
case "list_contracts": { case "list_contracts": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };
@ -346,6 +377,7 @@ export async function handleTool(
// ── Invoices (non-recurring) ── // ── Invoices (non-recurring) ──
case "list_invoices": { case "list_invoices": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };
@ -363,6 +395,7 @@ export async function handleTool(
// ── Recurring Invoices ── // ── Recurring Invoices ──
case "list_recurring_invoices": { case "list_recurring_invoices": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };
@ -378,6 +411,7 @@ export async function handleTool(
// ── Projects ── // ── Projects ──
case "list_projects": { case "list_projects": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };
@ -394,6 +428,7 @@ export async function handleTool(
// ── Actions / Time Entries ── // ── Actions / Time Entries ──
case "list_actions": { case "list_actions": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };
@ -409,6 +444,7 @@ export async function handleTool(
// ── Assets ── // ── Assets ──
case "list_assets": { case "list_assets": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };
@ -457,6 +493,7 @@ export async function handleTool(
// ── Agents ── // ── Agents ──
case "list_agents": { case "list_agents": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };
@ -468,6 +505,7 @@ export async function handleTool(
// ── Reports ── // ── Reports ──
case "list_reports": { case "list_reports": {
const params: Record<string, any> = { const params: Record<string, any> = {
pageinate: true,
page_no: args.page || 1, page_no: args.page || 1,
page_size: args.page_size || 50, page_size: args.page_size || 50,
}; };

5
start.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -a
source "$(dirname "$0")/.env"
set +a
exec node "$(dirname "$0")/dist/index.js"