diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2fd78ed --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Copy to .env and fill in real values: +# cp .env.example .env + +# API token with organizations:read, agents:read, and agents:write scopes +MAKE_API_KEY="REPLACE_WITH_YOUR_API_TOKEN" + +# Zone hostname only (no https://), e.g. eu2.make.com or us2.make.com +MAKE_ZONE="eu2.make.com" + +# Numeric organization ID (required for on-prem agent / connected-system tests) +MAKE_ORGANIZATION="12345" + +# Connected-system *create* integration (keys must match getAppConfig for each app) +MAKE_CONNECTED_SYSTEM_HTTP_INPUTS='{"url":"https://example.com"}' +MAKE_CONNECTED_SYSTEM_SAP_AGENT_INPUTS='{"ashost":"00","sysnr":"00","client":"00"}' diff --git a/README.md b/README.md index aebe0e3..017fb3a 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,19 @@ MAKE_TEAM="" MAKE_ORGANIZATION="" ``` +Required for connected-system **create** integration (field names from `getAppConfig`): + +``` +MAKE_CONNECTED_SYSTEM_HTTP_INPUTS='{"url":"https://example.com"}' +MAKE_CONNECTED_SYSTEM_SAP_AGENT_INPUTS='{"ashost":"00","sysnr":"00","client":"00"}' +``` + +Each create suite registers a **new** on-prem agent, then creates the connected system on that agent (no agent ID in `.env`). Use the same input field names and values you would enter in the Make UI. For `sap-agent`, the form uses `ashost`, `sysnr`, and `client` (not `language`); confirm names via `getAppConfig` if your zone differs. + +Read tests always cover **http** and **sap-agent**. Create suites run when the matching `MAKE_CONNECTED_SYSTEM_*_INPUTS` is set. + +Copy `.env.example` to `.env` and fill in values. The organization must have `license.onPremAgent` enabled; the API token needs `organizations:read`, `agents:read`, and `agents:write` scopes. + Please provide zone without `https://` prefix (e.g. `eu2.make.com`). ## Building diff --git a/jsr.json b/jsr.json index 992fa43..2c7e5cb 100644 --- a/jsr.json +++ b/jsr.json @@ -1,10 +1,17 @@ { "name": "@make/sdk", - "version": "0.0.0", + "version": "1.5.0", "exports": "./src/index.ts", "license": "MIT", "publish": { - "include": ["LICENSE", "README.md", "src/**/*.ts"], - "exclude": ["test/*", "scripts/*"] + "include": [ + "LICENSE", + "README.md", + "src/**/*.ts" + ], + "exclude": [ + "test/*", + "scripts/*" + ] } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 567cfcd..9114e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@makehq/sdk", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@makehq/sdk", - "version": "1.4.0", + "version": "1.5.0", "license": "MIT", "devDependencies": { "@eslint/js": "^9.22.0", diff --git a/package.json b/package.json index e5827af..e38018c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@makehq/sdk", - "version": "1.4.0", + "version": "1.5.0", "description": "Make TypeScript SDK", "license": "MIT", "author": "Make", diff --git a/src/endpoints/agents.ts b/src/endpoints/agents.ts new file mode 100644 index 0000000..a2ed905 --- /dev/null +++ b/src/endpoints/agents.ts @@ -0,0 +1,206 @@ +import type { FetchFunction } from '../types.js'; + +/** + * Status of an on-prem bridge agent. + */ +export type AgentStatus = 'ACTIVE' | 'STOPPED' | 'NOT_RESPONDING' | 'REGISTERED'; + +/** + * Represents a Make on-prem bridge agent (not Make AI `/v1/agents`). + */ +export type Agent = { + /** Unique identifier of the agent */ + id: string; + /** Tenant identifier in the agency service */ + tenantId: string; + /** User-defined name of the agent */ + name: string; + /** Client secret for agent authentication (sensitive; may only be shown at creation) */ + clientSecret?: string; + /** Current operational status */ + status: AgentStatus; + /** Whether the agent has been alerted */ + alerted: boolean; + /** Whether the agent is currently connected */ + connected: boolean; + /** Installed agent version */ + version?: string; + /** When the agent was created */ + createdDate?: string; + /** Last successful connection timestamp */ + lastConnectionDate?: string; + /** Number of connected systems using this agent */ + systemConnectionsCount?: number; +}; + +/** + * Parameters for registering a new on-prem agent. + */ +export type CreateAgentBody = { + /** Display name for the new agent */ + name: string; +}; + +/** + * Parameters for updating an on-prem agent. + */ +export type UpdateAgentBody = { + /** New name for the agent */ + name?: string; +}; + +/** + * Field definition returned for connected-system configuration on an agent app. + */ +export type AgentAppConfigField = { + /** Field identifier used in `inputs` when creating a connected system */ + name: string; + /** Human-readable label */ + label: string; + /** Help text, if any */ + help?: string | null; + /** Whether the field is required */ + required?: boolean; +}; + +/** + * Forman-style input descriptor for an agent app's connected-system form. + */ +export type AgentAppConfigInput = { + /** Top-level field name (typically `inputs` for a collection) */ + name: string; + /** Human-readable label */ + label: string; + /** Field type (e.g. `collection`) */ + type: string; + /** Nested field definitions when `type` is `collection` */ + spec?: AgentAppConfigField[]; +}; + +type ListAgentsResponse = { + agents: Agent[]; +}; + +type GetAgentResponse = { + agent: Agent; +}; + +type CreateAgentResponse = { + agent: Agent; +}; + +type UpdateAgentResponse = { + agent: Agent; +}; + +type DeleteAgentResponse = { + agent: string; +}; + +type GetAgentAppConfigResponse = { + inputs: AgentAppConfigInput[]; +}; + +/** + * Class providing methods for Make on-prem bridge agents. + * These agents run on customer infrastructure and connect to Make via the agency service. + */ +export class Agents { + readonly #fetch: FetchFunction; + + constructor(fetch: FetchFunction) { + this.#fetch = fetch; + } + + /** + * List on-prem agents for an organization. + * @param organizationId The organization ID + */ + async list(organizationId: number): Promise { + return ( + await this.#fetch('/agents', { + query: { organizationId }, + }) + ).agents; + } + + /** + * Get details of a specific on-prem agent. + * @param organizationId The organization ID + * @param agentId The agent UUID + */ + async get(organizationId: number, agentId: string): Promise { + return ( + await this.#fetch(`/agents/${agentId}`, { + query: { organizationId }, + }) + ).agent; + } + + /** + * Register a new on-prem agent. The server assigns `id` and `clientSecret`. + * @param organizationId The organization ID + * @param body Agent registration parameters (`name` only) + */ + async create(organizationId: number, body: CreateAgentBody): Promise { + return ( + await this.#fetch('/agent/register', { + method: 'POST', + query: { organizationId }, + body, + }) + ).agent; + } + + /** + * Update an on-prem agent (currently supports renaming via `name`). + * @param organizationId The organization ID + * @param agentId The agent UUID + * @param body Fields to update + */ + async update(organizationId: number, agentId: string, body: UpdateAgentBody): Promise { + return ( + await this.#fetch(`/agents/${agentId}`, { + method: 'PATCH', + query: { organizationId }, + body, + }) + ).agent; + } + + /** + * Delete an on-prem agent. + * @param organizationId The organization ID + * @param agentId The agent UUID + * @returns The deleted agent ID + */ + async delete(organizationId: number, agentId: string): Promise { + return ( + await this.#fetch(`/agents/${agentId}`, { + method: 'DELETE', + query: { organizationId }, + }) + ).agent; + } + + /** + * Get dynamic input field definitions for creating a connected system on an agent app. + * @param organizationId The organization ID + * @param agentId The agent UUID + * @param appName Connected-system app slug (e.g. `http`, `sap-agent`) + */ + async getAppConfig( + organizationId: number, + agentId: string, + appName: string, + ): Promise { + return ( + await this.#fetch( + `/agents/${agentId}/apps/${appName}/config`, + { + query: { organizationId }, + }, + ) + ).inputs; + } +} diff --git a/src/endpoints/connected-systems.tools.ts b/src/endpoints/connected-systems.tools.ts new file mode 100644 index 0000000..dc048e3 --- /dev/null +++ b/src/endpoints/connected-systems.tools.ts @@ -0,0 +1,206 @@ +import type { Make } from '../make.js'; +import type { JSONValue } from '../types.js'; +import type { MakeTool } from '../tools.js'; + +export const tools: MakeTool[] = [ + { + name: 'connected-system_list', + title: 'List connected systems', + description: + 'List on-prem connected systems for a specific on-prem agent. Requires both organizationId and agentId.', + category: 'connected-system', + scope: 'agents:read', + scopeId: 'organizationId', + identifier: 'organizationId', + annotations: { + readOnlyHint: true, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + agentId: { type: 'string', description: 'The on-prem agent UUID' }, + }, + required: ['organizationId', 'agentId'], + }, + examples: [ + { + organizationId: 5, + agentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + }, + ], + execute: async (make: Make, args: { organizationId: number; agentId: string }) => { + return await make.connectedSystems.list(args.organizationId, args.agentId); + }, + }, + { + name: 'connected-system_get', + title: 'Get connected system', + description: 'Get details of an on-prem connected system.', + category: 'connected-system', + scope: 'agents:read', + scopeId: 'organizationId', + identifier: 'organizationId', + resourceId: 'connectedSystemId', + annotations: { + readOnlyHint: true, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + connectedSystemId: { type: 'string', description: 'The connected system UUID' }, + }, + required: ['organizationId', 'connectedSystemId'], + }, + examples: [ + { + organizationId: 5, + connectedSystemId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901', + }, + ], + execute: async (make: Make, args: { organizationId: number; connectedSystemId: string }) => { + return await make.connectedSystems.get(args.organizationId, args.connectedSystemId); + }, + }, + { + name: 'connected-system_create', + title: 'Create connected system', + description: + 'Create an on-prem connected system on an agent. First use connected-system_list-apps to pick appName, then on-prem-agent_get-app-config to discover required input keys. Supply inputs as a keyed object matching those fields.', + category: 'connected-system', + scope: 'agents:write', + scopeId: 'organizationId', + identifier: 'organizationId', + annotations: { + idempotentHint: false, + destructiveHint: false, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + name: { type: 'string', description: 'Display name for the connected system' }, + agentId: { type: 'string', description: 'On-prem agent UUID that hosts the connection' }, + appName: { type: 'string', description: 'App slug from connected-system_list-apps' }, + inputs: { + type: 'object', + description: 'App-specific configuration values keyed by field name', + }, + }, + required: ['organizationId', 'name', 'agentId', 'appName', 'inputs'], + }, + examples: [ + { + organizationId: 5, + name: 'SAP production', + agentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + appName: 'sap-agent', + inputs: { ashost: '10.0.0.150', sysnr: '00', client: '100' }, + }, + ], + execute: async ( + make: Make, + args: { + organizationId: number; + name: string; + agentId: string; + appName: string; + inputs: Record; + }, + ) => { + return await make.connectedSystems.create(args.organizationId, { + name: args.name, + agentId: args.agentId, + appName: args.appName, + inputs: args.inputs, + }); + }, + }, + { + name: 'connected-system_update', + title: 'Update connected system', + description: 'Update an on-prem connected system. All body fields are optional.', + category: 'connected-system', + scope: 'agents:write', + scopeId: 'organizationId', + identifier: 'organizationId', + resourceId: 'connectedSystemId', + annotations: { + idempotentHint: true, + destructiveHint: false, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + connectedSystemId: { type: 'string', description: 'The connected system UUID' }, + name: { type: 'string', description: 'New display name' }, + agentId: { type: 'string', description: 'Move to a different on-prem agent' }, + appName: { type: 'string', description: 'Change the app slug' }, + inputs: { + type: 'object', + description: 'Updated configuration values keyed by field name', + }, + }, + required: ['organizationId', 'connectedSystemId'], + }, + examples: [ + { + organizationId: 5, + connectedSystemId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901', + name: 'SAP staging', + inputs: { ashost: '10.0.0.150', sysnr: '00', client: '100' }, + }, + ], + execute: async ( + make: Make, + args: { + organizationId: number; + connectedSystemId: string; + name?: string; + agentId?: string; + appName?: string; + inputs?: Record; + }, + ) => { + return await make.connectedSystems.update(args.organizationId, args.connectedSystemId, { + name: args.name, + agentId: args.agentId, + appName: args.appName, + inputs: args.inputs, + }); + }, + }, + { + name: 'connected-system_delete', + title: 'Delete connected system', + description: 'Delete an on-prem connected system. This is destructive and cannot be undone.', + category: 'connected-system', + scope: 'agents:write', + scopeId: 'organizationId', + identifier: 'organizationId', + resourceId: 'connectedSystemId', + annotations: { + destructiveHint: true, + idempotentHint: true, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + connectedSystemId: { type: 'string', description: 'The connected system UUID' }, + }, + required: ['organizationId', 'connectedSystemId'], + }, + examples: [ + { + organizationId: 5, + connectedSystemId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901', + }, + ], + execute: async (make: Make, args: { organizationId: number; connectedSystemId: string }) => { + return await make.connectedSystems.delete(args.organizationId, args.connectedSystemId); + }, + }, +]; diff --git a/src/endpoints/connected-systems.ts b/src/endpoints/connected-systems.ts new file mode 100644 index 0000000..471e12c --- /dev/null +++ b/src/endpoints/connected-systems.ts @@ -0,0 +1,165 @@ +import type { FetchFunction, JSONValue } from '../types.js'; + +/** + * A single valued input on a connected system. + */ +export type ConnectedSystemInput = { + /** Input field name */ + name: string; + /** Input value */ + value: string; +}; + +/** + * Represents a connected system (on-prem bridge connection to an external app). + */ +export type ConnectedSystem = { + /** Unique identifier of the connected system */ + id: string; + /** User-defined name */ + name: string; + /** ID of the on-prem agent hosting this connection */ + agentId: string; + /** Connected-system app slug */ + appName: string; + /** Agent version at time of connection */ + agentVersion?: string; + /** Configured inputs */ + inputs?: ConnectedSystemInput[]; +}; + +/** + * Parameters for creating a connected system. + * `inputs` is a keyed object matching the app's dynamic form (see `Agents.getAppConfig`). + */ +export type CreateConnectedSystemBody = { + /** Display name for the connected system */ + name: string; + /** On-prem agent UUID that will host the connection */ + agentId: string; + /** App slug from `Enums.connectedSystemApps()` */ + appName: string; + /** App-specific configuration values keyed by field name */ + inputs: Record; +}; + +/** + * Parameters for updating a connected system. + */ +export type UpdateConnectedSystemBody = { + /** New display name */ + name?: string; + /** Move to a different agent */ + agentId?: string; + /** Change the app (uncommon after creation) */ + appName?: string; + /** Updated configuration values keyed by field name */ + inputs?: Record; +}; + +type ListConnectedSystemsResponse = { + connectedSystems: ConnectedSystem[]; +}; + +type GetConnectedSystemResponse = { + connectedSystem: ConnectedSystem; +}; + +type CreateConnectedSystemResponse = { + connectedSystem: ConnectedSystem; +}; + +type UpdateConnectedSystemResponse = { + connectedSystem: ConnectedSystem; +}; + +type DeleteConnectedSystemResponse = { + connectedSystem: string; +}; + +/** + * Class providing methods for Make on-prem connected systems. + */ +export class ConnectedSystems { + readonly #fetch: FetchFunction; + + constructor(fetch: FetchFunction) { + this.#fetch = fetch; + } + + /** + * List connected systems for an on-prem agent. + * @param organizationId The organization ID + * @param agentId The on-prem agent UUID + */ + async list(organizationId: number, agentId: string): Promise { + return ( + await this.#fetch('/connected-systems', { + query: { organizationId, agentId }, + }) + ).connectedSystems; + } + + /** + * Get details of a connected system. + * @param organizationId The organization ID + * @param connectedSystemId The connected system UUID + */ + async get(organizationId: number, connectedSystemId: string): Promise { + return ( + await this.#fetch(`/connected-systems/${connectedSystemId}`, { + query: { organizationId }, + }) + ).connectedSystem; + } + + /** + * Create a connected system on an on-prem agent. + * @param organizationId The organization ID + * @param body Creation parameters including app-specific `inputs` + */ + async create(organizationId: number, body: CreateConnectedSystemBody): Promise { + return ( + await this.#fetch('/connected-systems', { + method: 'POST', + query: { organizationId }, + body, + }) + ).connectedSystem; + } + + /** + * Update a connected system. + * @param organizationId The organization ID + * @param connectedSystemId The connected system UUID + * @param body Fields to update + */ + async update( + organizationId: number, + connectedSystemId: string, + body: UpdateConnectedSystemBody, + ): Promise { + return ( + await this.#fetch(`/connected-systems/${connectedSystemId}`, { + method: 'PATCH', + query: { organizationId }, + body, + }) + ).connectedSystem; + } + + /** + * Delete a connected system. + * @param organizationId The organization ID + * @param connectedSystemId The connected system UUID + * @returns The deleted connected system ID + */ + async delete(organizationId: number, connectedSystemId: string): Promise { + return ( + await this.#fetch(`/connected-systems/${connectedSystemId}`, { + method: 'DELETE', + query: { organizationId }, + }) + ).connectedSystem; + } +} diff --git a/src/endpoints/enums.tools.ts b/src/endpoints/enums.tools.ts index 4660599..ed9abc9 100644 --- a/src/endpoints/enums.tools.ts +++ b/src/endpoints/enums.tools.ts @@ -62,4 +62,25 @@ export const tools: MakeTool[] = [ return await make.enums.timezones(); }, }, + { + name: 'connected-system_list-apps', + title: 'List connected-system apps', + description: + 'List available app slugs for on-prem connected systems (e.g. http, sap-agent). Use before creating a connected system.', + category: 'connected-system', + scope: 'agents:read', + scopeId: undefined, + identifier: undefined, + annotations: { + readOnlyHint: true, + }, + inputSchema: { + type: 'object', + properties: {}, + }, + examples: [{}], + execute: async (make: Make) => { + return await make.enums.connectedSystemApps(); + }, + }, ]; diff --git a/src/endpoints/enums.ts b/src/endpoints/enums.ts index 04c83dc..205c4ec 100644 --- a/src/endpoints/enums.ts +++ b/src/endpoints/enums.ts @@ -29,6 +29,18 @@ export type Timezone = { offset: string; }; +/** + * On-prem connected-system app available for bridge agents. + */ +export type ConnectedSystemApp = { + /** App slug used as `appName` when creating a connected system */ + name: string; + /** Human-readable label */ + label: string; + /** Icon identifier or URL */ + icon: string; +}; + type ListCountriesResponse = { /** List of countries */ countries: Country[]; @@ -44,6 +56,11 @@ type ListTimezonesResponse = { timezones: Timezone[]; }; +type ListConnectedSystemAppsResponse = { + /** List of connected-system apps supported on on-prem agents */ + connectedSystemApps: ConnectedSystemApp[]; +}; + /** * Class providing methods for working with Make enums. * Enums provide access to standardized lists of values like countries, regions, and timezones. @@ -82,4 +99,14 @@ export class Enums { async timezones(): Promise { return (await this.#fetch('/enums/timezones')).timezones; } + + /** + * Get list of apps available for on-prem connected systems (e.g. `http`, `sap-agent`). + * @returns Promise with the list of connected-system apps + */ + async connectedSystemApps(): Promise { + return ( + await this.#fetch('/enums/connected-system-apps') + ).connectedSystemApps; + } } diff --git a/src/endpoints/on-prem-agents.tools.ts b/src/endpoints/on-prem-agents.tools.ts new file mode 100644 index 0000000..7a8b3a4 --- /dev/null +++ b/src/endpoints/on-prem-agents.tools.ts @@ -0,0 +1,172 @@ +import type { Make } from '../make.js'; +import type { MakeTool } from '../tools.js'; + +export const tools: MakeTool[] = [ + { + name: 'on-prem-agent_list', + title: 'List on-prem agents', + description: + 'List on-prem bridge agents for an organization. These are customer-hosted agents, not Make AI agents.', + category: 'on-prem-agent', + scope: 'agents:read', + scopeId: 'organizationId', + identifier: 'organizationId', + annotations: { + readOnlyHint: true, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + }, + required: ['organizationId'], + }, + examples: [{ organizationId: 5 }], + execute: async (make: Make, args: { organizationId: number }) => { + return await make.agents.list(args.organizationId); + }, + }, + { + name: 'on-prem-agent_get', + title: 'Get on-prem agent', + description: 'Get details of a specific on-prem bridge agent.', + category: 'on-prem-agent', + scope: 'agents:read', + scopeId: 'organizationId', + identifier: 'organizationId', + resourceId: 'agentId', + annotations: { + readOnlyHint: true, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + agentId: { type: 'string', description: 'The on-prem agent UUID' }, + }, + required: ['organizationId', 'agentId'], + }, + examples: [{ organizationId: 5, agentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }], + execute: async (make: Make, args: { organizationId: number; agentId: string }) => { + return await make.agents.get(args.organizationId, args.agentId); + }, + }, + { + name: 'on-prem-agent_create', + title: 'Register on-prem agent', + description: + 'Register a new on-prem bridge agent. The server assigns id and clientSecret; store the secret securely — it may only be shown once.', + category: 'on-prem-agent', + scope: 'agents:write', + scopeId: 'organizationId', + identifier: 'organizationId', + annotations: { + idempotentHint: false, + destructiveHint: false, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + name: { type: 'string', description: 'Display name for the new on-prem agent' }, + }, + required: ['organizationId', 'name'], + }, + examples: [{ organizationId: 5, name: 'Production bridge' }], + execute: async (make: Make, args: { organizationId: number; name: string }) => { + return await make.agents.create(args.organizationId, { name: args.name }); + }, + }, + { + name: 'on-prem-agent_update', + title: 'Update on-prem agent', + description: 'Update an on-prem bridge agent (currently supports renaming).', + category: 'on-prem-agent', + scope: 'agents:write', + scopeId: 'organizationId', + identifier: 'organizationId', + resourceId: 'agentId', + annotations: { + idempotentHint: true, + destructiveHint: false, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + agentId: { type: 'string', description: 'The on-prem agent UUID' }, + name: { type: 'string', description: 'New display name for the agent' }, + }, + required: ['organizationId', 'agentId'], + }, + examples: [{ organizationId: 5, agentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', name: 'Renamed agent' }], + execute: async (make: Make, args: { organizationId: number; agentId: string; name?: string }) => { + return await make.agents.update(args.organizationId, args.agentId, { name: args.name }); + }, + }, + { + name: 'on-prem-agent_delete', + title: 'Delete on-prem agent', + description: 'Delete an on-prem bridge agent. This is destructive and cannot be undone.', + category: 'on-prem-agent', + scope: 'agents:write', + scopeId: 'organizationId', + identifier: 'organizationId', + resourceId: 'agentId', + annotations: { + destructiveHint: true, + idempotentHint: true, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + agentId: { type: 'string', description: 'The on-prem agent UUID' }, + }, + required: ['organizationId', 'agentId'], + }, + examples: [{ organizationId: 5, agentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' }], + execute: async (make: Make, args: { organizationId: number; agentId: string }) => { + return await make.agents.delete(args.organizationId, args.agentId); + }, + }, + { + name: 'on-prem-agent_get-app-config', + title: 'Get on-prem agent app config', + description: + 'Get dynamic input field definitions for creating a connected system on an on-prem agent app. Use connected-system_list-apps to choose appName, then supply keyed inputs when creating a connected system.', + category: 'on-prem-agent', + scope: 'agents:read', + scopeId: 'organizationId', + identifier: 'organizationId', + resourceId: 'agentId', + annotations: { + readOnlyHint: true, + }, + inputSchema: { + type: 'object', + properties: { + organizationId: { type: 'number', description: 'The organization ID' }, + agentId: { type: 'string', description: 'The on-prem agent UUID' }, + appName: { + type: 'string', + description: 'Connected-system app slug (e.g. http, sap-agent)', + }, + }, + required: ['organizationId', 'agentId', 'appName'], + }, + examples: [ + { + organizationId: 5, + agentId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890', + appName: 'http', + }, + ], + execute: async ( + make: Make, + args: { organizationId: number; agentId: string; appName: string }, + ) => { + return await make.agents.getAppConfig(args.organizationId, args.agentId, args.appName); + }, + }, +]; diff --git a/src/index.ts b/src/index.ts index dff4846..080138d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,7 +62,23 @@ export type { CloneDataStructureBody, } from './endpoints/data-structures.js'; export type { Device, DeviceInfo, Devices, ListDevicesOptions } from './endpoints/devices.js'; -export type { Enums, Country, Region, Timezone } from './endpoints/enums.js'; +export type { Enums, Country, Region, Timezone, ConnectedSystemApp } from './endpoints/enums.js'; +export type { + Agent, + Agents, + AgentStatus, + AgentAppConfigField, + AgentAppConfigInput, + CreateAgentBody, + UpdateAgentBody, +} from './endpoints/agents.js'; +export type { + ConnectedSystem, + ConnectedSystems, + ConnectedSystemInput, + CreateConnectedSystemBody, + UpdateConnectedSystemBody, +} from './endpoints/connected-systems.js'; export type { Execution, ExecutionDetail, Executions, ListExecutionsOptions } from './endpoints/executions.js'; export type { Folder, diff --git a/src/make.ts b/src/make.ts index e1209a7..b51ce91 100644 --- a/src/make.ts +++ b/src/make.ts @@ -15,6 +15,8 @@ import { Devices } from './endpoints/devices.js'; import { Functions } from './endpoints/functions.js'; import { Organizations } from './endpoints/organizations.js'; import { Enums } from './endpoints/enums.js'; +import { Agents } from './endpoints/agents.js'; +import { ConnectedSystems } from './endpoints/connected-systems.js'; import { PublicTemplates } from './endpoints/public-templates.js'; import { SDKApps } from './endpoints/sdk/apps.js'; import { SDKModules } from './endpoints/sdk/modules.js'; @@ -164,6 +166,18 @@ export class Make { */ public readonly enums: Enums; + /** + * Access to on-prem bridge agent endpoints + * On-prem agents run on customer infrastructure and connect to Make via the agency service + */ + public readonly agents: Agents; + + /** + * Access to on-prem connected system endpoints + * Connected systems link on-prem agents to external apps (HTTP, SAP, etc.) + */ + public readonly connectedSystems: ConnectedSystems; + /** * Access to public template endpoints. * Public templates are approved, read-only scenario configurations discoverable and usable by any Make user. @@ -254,6 +268,8 @@ export class Make { this.functions = new Functions(this.fetch.bind(this)); this.organizations = new Organizations(this.fetch.bind(this)); this.enums = new Enums(this.fetch.bind(this)); + this.agents = new Agents(this.fetch.bind(this)); + this.connectedSystems = new ConnectedSystems(this.fetch.bind(this)); this.credentialRequests = new CredentialRequests(this.fetch.bind(this)); this.publicTemplates = new PublicTemplates(this.fetch.bind(this)); this.sdk = { diff --git a/src/tools.ts b/src/tools.ts index 01ad724..d247d3a 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -25,6 +25,8 @@ import { tools as FoldersTools } from './endpoints/folders.tools.js'; import { tools as IncompleteExecutionsTools } from './endpoints/incomplete-executions.tools.js'; import { tools as DataStructuresTools } from './endpoints/data-structures.tools.js'; import { tools as EnumsTools } from './endpoints/enums.tools.js'; +import { tools as OnPremAgentsTools } from './endpoints/on-prem-agents.tools.js'; +import { tools as ConnectedSystemsTools } from './endpoints/connected-systems.tools.js'; import { tools as PublicTemplatesTools } from './endpoints/public-templates.tools.js'; /** @@ -214,6 +216,8 @@ export const MakeTools = [ ...TeamsTools, ...OrganizationsTools, ...UsersTools, + ...OnPremAgentsTools, + ...ConnectedSystemsTools, ...EnumsTools, ...PublicTemplatesTools, ] as MakeTool[]; diff --git a/src/version.ts b/src/version.ts index 6dabcc5..e10d487 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = 'development'; // To be replaced while publishing +export const VERSION = '1.5.0'; \ No newline at end of file diff --git a/test/agents.integration.test.ts b/test/agents.integration.test.ts new file mode 100644 index 0000000..bcbe162 --- /dev/null +++ b/test/agents.integration.test.ts @@ -0,0 +1,88 @@ +import 'dotenv/config'; +import { afterAll, describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeTools } from '../src/tools.js'; + +const MAKE_API_KEY = String(process.env.MAKE_API_KEY || ''); +const MAKE_ZONE = String(process.env.MAKE_ZONE || ''); +const MAKE_ORGANIZATION = Number(process.env.MAKE_ORGANIZATION || 0); + +const CONNECTED_SYSTEM_APPS = ['http', 'sap-agent'] as const; + +const integrationReady = Boolean(MAKE_API_KEY && MAKE_ZONE && MAKE_ORGANIZATION); + +/** Valid UUID that is unlikely to exist in the test org */ +const NON_EXISTENT_AGENT_ID = '00000000-0000-4000-8000-000000000000'; + +(integrationReady ? describe : describe.skip)('Integration: Agents (on-prem)', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + let agentId: string; + const agentName = `SDK integration agent ${Date.now()}`; + + it('Should verify on-prem agent license on the organization', async () => { + const org = await make.organizations.get(MAKE_ORGANIZATION); + expect(org.license?.onPremAgent).toBe(true); + }); + + it('Should register an on-prem agent via /agent/register', async () => { + const agent = await make.agents.create(MAKE_ORGANIZATION, { name: agentName }); + + expect(agent.id).toBeDefined(); + expect(agent.name).toBe(agentName); + expect(agent.status).toBeDefined(); + + agentId = agent.id; + }); + + it('Should list on-prem agents', async () => { + const agents = await make.agents.list(MAKE_ORGANIZATION); + + expect(Array.isArray(agents)).toBe(true); + expect(agents.some(a => a.id === agentId)).toBe(true); + }); + + it('Should get an on-prem agent', async () => { + const agent = await make.agents.get(MAKE_ORGANIZATION, agentId); + + expect(agent.id).toBe(agentId); + expect(agent.name).toBe(agentName); + }); + + it('Should update an on-prem agent name', async () => { + const updatedName = `${agentName} updated`; + const agent = await make.agents.update(MAKE_ORGANIZATION, agentId, { name: updatedName }); + + expect(agent.id).toBe(agentId); + expect(agent.name).toBe(updatedName); + }); + + it.each(CONNECTED_SYSTEM_APPS)('Should get app config for %s', async appName => { + const inputs = await make.agents.getAppConfig(MAKE_ORGANIZATION, agentId, appName); + + expect(Array.isArray(inputs)).toBe(true); + expect(inputs.length).toBeGreaterThan(0); + }); + + it('Should execute on-prem-agent_list MCP tool', async () => { + const tool = MakeTools.find(entry => entry.name === 'on-prem-agent_list'); + expect(tool).toBeDefined(); + + const result = await tool!.execute(make, { organizationId: MAKE_ORGANIZATION }); + expect(Array.isArray(result)).toBe(true); + expect((result as { id: string }[]).some(agent => agent.id === agentId)).toBe(true); + }); + + it('Should throw MakeError for a non-existent on-prem agent', async () => { + await expect(make.agents.get(MAKE_ORGANIZATION, NON_EXISTENT_AGENT_ID)).rejects.toMatchObject({ + name: 'MakeError', + statusCode: 400, + }); + }); + + afterAll(async () => { + if (agentId) { + await make.agents.delete(MAKE_ORGANIZATION, agentId); + } + }); +}); diff --git a/test/agents.spec.ts b/test/agents.spec.ts new file mode 100644 index 0000000..e7e455d --- /dev/null +++ b/test/agents.spec.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeError } from '../src/utils.js'; +import { mockFetch } from './test.utils.js'; + +import * as agentsListMock from './mocks/agents/list.json'; +import * as agentGetMock from './mocks/agents/get.json'; +import * as agentCreateMock from './mocks/agents/create.json'; +import * as agentUpdateMock from './mocks/agents/update.json'; +import * as agentDeleteMock from './mocks/agents/delete.json'; +import * as agentAppConfigMock from './mocks/agents/app-config.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; +const ORGANIZATION_ID = 5; +const AGENT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; +const APP_NAME = 'sap-agent'; + +describe('Endpoints: Agents (on-prem)', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should list on-prem agents', async () => { + mockFetch(`GET https://make.local/api/v2/agents?organizationId=${ORGANIZATION_ID}`, agentsListMock); + + const result = await make.agents.list(ORGANIZATION_ID); + expect(result).toStrictEqual(agentsListMock.agents); + }); + + it('Should get an on-prem agent', async () => { + mockFetch( + `GET https://make.local/api/v2/agents/${AGENT_ID}?organizationId=${ORGANIZATION_ID}`, + agentGetMock, + ); + + const result = await make.agents.get(ORGANIZATION_ID, AGENT_ID); + expect(result).toStrictEqual(agentGetMock.agent); + }); + + it('Should register an on-prem agent via /agent/register', async () => { + const body = { name: 'New bridge' }; + + mockFetch( + `POST https://make.local/api/v2/agent/register?organizationId=${ORGANIZATION_ID}`, + agentCreateMock, + req => { + expect(req.body).toStrictEqual(body); + }, + ); + + const result = await make.agents.create(ORGANIZATION_ID, body); + expect(result).toStrictEqual(agentCreateMock.agent); + }); + + it('Should update an on-prem agent', async () => { + const body = { name: 'Renamed bridge' }; + + mockFetch( + `PATCH https://make.local/api/v2/agents/${AGENT_ID}?organizationId=${ORGANIZATION_ID}`, + agentUpdateMock, + req => { + expect(req.body).toStrictEqual(body); + }, + ); + + const result = await make.agents.update(ORGANIZATION_ID, AGENT_ID, body); + expect(result).toStrictEqual(agentUpdateMock.agent); + }); + + it('Should delete an on-prem agent', async () => { + mockFetch( + `DELETE https://make.local/api/v2/agents/${AGENT_ID}?organizationId=${ORGANIZATION_ID}`, + agentDeleteMock, + ); + + const result = await make.agents.delete(ORGANIZATION_ID, AGENT_ID); + expect(result).toStrictEqual(agentDeleteMock.agent); + }); + + it('Should get app config for connected-system inputs', async () => { + mockFetch( + `GET https://make.local/api/v2/agents/${AGENT_ID}/apps/${APP_NAME}/config?organizationId=${ORGANIZATION_ID}`, + agentAppConfigMock, + ); + + const result = await make.agents.getAppConfig(ORGANIZATION_ID, AGENT_ID, APP_NAME); + expect(result).toStrictEqual(agentAppConfigMock.inputs); + }); + + it('Should throw MakeError when the agent is not found', async () => { + mockFetch( + `GET https://make.local/api/v2/agents/${AGENT_ID}?organizationId=${ORGANIZATION_ID}`, + { message: 'Not found' }, + 404, + ); + + await expect(make.agents.get(ORGANIZATION_ID, AGENT_ID)).rejects.toMatchObject({ + name: 'MakeError', + statusCode: 404, + message: 'Not found', + }); + }); + + it('Should throw MakeError when agent registration fails validation', async () => { + mockFetch( + `POST https://make.local/api/v2/agent/register?organizationId=${ORGANIZATION_ID}`, + { + message: 'Validation failed', + suberrors: [{ message: 'Name is required' }], + }, + 422, + ); + + await expect(make.agents.create(ORGANIZATION_ID, { name: '' })).rejects.toBeInstanceOf(MakeError); + }); +}); diff --git a/test/connected-systems.integration.test.ts b/test/connected-systems.integration.test.ts new file mode 100644 index 0000000..af454f2 --- /dev/null +++ b/test/connected-systems.integration.test.ts @@ -0,0 +1,181 @@ +import 'dotenv/config'; +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeTools } from '../src/tools.js'; +import { + assertInputsMatchAppConfig, + parseInputs, +} from './on-prem-connected-system.utils.js'; + +const MAKE_API_KEY = String(process.env.MAKE_API_KEY || ''); +const MAKE_ZONE = String(process.env.MAKE_ZONE || ''); +const MAKE_ORGANIZATION = Number(process.env.MAKE_ORGANIZATION || 0); + +const CONNECTED_SYSTEM_APPS = ['http', 'sap-agent'] as const; + +type ConnectedSystemAppName = (typeof CONNECTED_SYSTEM_APPS)[number]; + +const CREATE_INPUTS_ENV: Record = { + http: 'MAKE_CONNECTED_SYSTEM_HTTP_INPUTS', + 'sap-agent': 'MAKE_CONNECTED_SYSTEM_SAP_AGENT_INPUTS', +}; + +const integrationReady = Boolean(MAKE_API_KEY && MAKE_ZONE && MAKE_ORGANIZATION); + +/** Valid UUID that is unlikely to exist in the test org */ +const NON_EXISTENT_RESOURCE_ID = '00000000-0000-4000-8000-000000000000'; + +function createSuiteEnabled(appName: ConnectedSystemAppName): boolean { + return integrationReady && Boolean(process.env[CREATE_INPUTS_ENV[appName]]); +} + +function getTool(name: string) { + const tool = MakeTools.find(entry => entry.name === name); + if (!tool) { + throw new Error(`Missing MCP tool: ${name}`); + } + return tool; +} + +(integrationReady ? describe : describe.skip)('Integration: Connected systems (on-prem, read)', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + let agentId: string; + + it('Should verify on-prem agent license on the organization', async () => { + const org = await make.organizations.get(MAKE_ORGANIZATION); + expect(org.license?.onPremAgent).toBe(true); + }); + + it('Should provision a temporary on-prem agent for connected-system reads', async () => { + const agent = await make.agents.create(MAKE_ORGANIZATION, { + name: `SDK integration CS agent ${Date.now()}`, + }); + agentId = agent.id; + expect(agentId).toBeDefined(); + }); + + it('Should list http and sap-agent in connected-system apps', async () => { + const apps = await make.enums.connectedSystemApps(); + const names = apps.map(app => app.name); + + expect(Array.isArray(apps)).toBe(true); + for (const appName of CONNECTED_SYSTEM_APPS) { + expect(names).toContain(appName); + } + }); + + it.each(CONNECTED_SYSTEM_APPS)('Should get app config for %s', async appName => { + const config = await make.agents.getAppConfig(MAKE_ORGANIZATION, agentId, appName); + + expect(Array.isArray(config)).toBe(true); + expect(config.length).toBeGreaterThan(0); + }); + + it('Should list connected systems for the agent', async () => { + const systems = await make.connectedSystems.list(MAKE_ORGANIZATION, agentId); + + expect(Array.isArray(systems)).toBe(true); + }); + + it('Should execute connected-system_list MCP tool', async () => { + const tool = getTool('connected-system_list'); + const systems = await tool.execute(make, { + organizationId: MAKE_ORGANIZATION, + agentId, + }); + + expect(Array.isArray(systems)).toBe(true); + }); + + it('Should throw MakeError for a non-existent connected system', async () => { + await expect( + make.connectedSystems.get(MAKE_ORGANIZATION, NON_EXISTENT_RESOURCE_ID), + ).rejects.toMatchObject({ name: 'MakeError', statusCode: 400 }); + }); + + afterAll(async () => { + if (agentId) { + await make.agents.delete(MAKE_ORGANIZATION, agentId); + } + }); +}); + +for (const appName of CONNECTED_SYSTEM_APPS) { + const inputsEnv = CREATE_INPUTS_ENV[appName]; + const inputsRaw = process.env[inputsEnv]; + const suiteReady = createSuiteEnabled(appName); + const skipHint = suiteReady ? '' : ` — set ${inputsEnv} in .env`; + + (suiteReady ? describe : describe.skip)( + `Integration: Connected systems (on-prem, create ${appName})${skipHint}`, + () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + let agentId: string; + let connectedSystemId: string; + const systemName = `SDK integration ${appName} ${Date.now()}`; + + beforeAll(async () => { + const agent = await make.agents.create(MAKE_ORGANIZATION, { + name: `SDK integration ${appName} create ${Date.now()}`, + }); + agentId = agent.id; + + const config = await make.agents.getAppConfig(MAKE_ORGANIZATION, agentId, appName); + assertInputsMatchAppConfig( + config, + parseInputs(inputsRaw, inputsEnv), + inputsEnv, + appName, + ); + }); + + it(`Should create a ${appName} connected system`, async () => { + const connectedSystem = await make.connectedSystems.create(MAKE_ORGANIZATION, { + name: systemName, + agentId, + appName, + inputs: parseInputs(inputsRaw, inputsEnv), + }); + + expect(connectedSystem.id).toBeDefined(); + expect(connectedSystem.name).toBe(systemName); + expect(connectedSystem.agentId).toBe(agentId); + expect(connectedSystem.appName).toBe(appName); + + connectedSystemId = connectedSystem.id; + }); + + it(`Should get the ${appName} connected system`, async () => { + const connectedSystem = await make.connectedSystems.get( + MAKE_ORGANIZATION, + connectedSystemId, + ); + + expect(connectedSystem.id).toBe(connectedSystemId); + expect(connectedSystem.appName).toBe(appName); + }); + + it(`Should update the ${appName} connected system name`, async () => { + const updatedName = `${systemName} updated`; + const connectedSystem = await make.connectedSystems.update( + MAKE_ORGANIZATION, + connectedSystemId, + { name: updatedName }, + ); + + expect(connectedSystem.name).toBe(updatedName); + }); + + afterAll(async () => { + if (connectedSystemId) { + await make.connectedSystems.delete(MAKE_ORGANIZATION, connectedSystemId); + } + if (agentId) { + await make.agents.delete(MAKE_ORGANIZATION, agentId); + } + }); + }, + ); +} diff --git a/test/connected-systems.spec.ts b/test/connected-systems.spec.ts new file mode 100644 index 0000000..ed94f3f --- /dev/null +++ b/test/connected-systems.spec.ts @@ -0,0 +1,116 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeError } from '../src/utils.js'; +import { mockFetch } from './test.utils.js'; + +import * as connectedSystemsListMock from './mocks/connected-systems/list.json'; +import * as connectedSystemGetMock from './mocks/connected-systems/get.json'; +import * as connectedSystemCreateMock from './mocks/connected-systems/create.json'; +import * as connectedSystemUpdateMock from './mocks/connected-systems/update.json'; +import * as connectedSystemDeleteMock from './mocks/connected-systems/delete.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; +const ORGANIZATION_ID = 5; +const AGENT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; +const CONNECTED_SYSTEM_ID = 'b2c3d4e5-f6a7-8901-bcde-f12345678901'; + +describe('Endpoints: Connected systems (on-prem)', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should list connected systems for an agent', async () => { + mockFetch( + `GET https://make.local/api/v2/connected-systems?organizationId=${ORGANIZATION_ID}&agentId=${AGENT_ID}`, + connectedSystemsListMock, + ); + + const result = await make.connectedSystems.list(ORGANIZATION_ID, AGENT_ID); + expect(result).toStrictEqual(connectedSystemsListMock.connectedSystems); + }); + + it('Should get a connected system', async () => { + mockFetch( + `GET https://make.local/api/v2/connected-systems/${CONNECTED_SYSTEM_ID}?organizationId=${ORGANIZATION_ID}`, + connectedSystemGetMock, + ); + + const result = await make.connectedSystems.get(ORGANIZATION_ID, CONNECTED_SYSTEM_ID); + expect(result).toStrictEqual(connectedSystemGetMock.connectedSystem); + }); + + it('Should create a connected system with keyed inputs object', async () => { + const body = { + name: 'SAP production', + agentId: AGENT_ID, + appName: 'sap-agent', + inputs: { ashost: '00', sysnr: '00', client: '00' }, + }; + + mockFetch( + `POST https://make.local/api/v2/connected-systems?organizationId=${ORGANIZATION_ID}`, + connectedSystemCreateMock, + req => { + expect(req.body).toStrictEqual(body); + }, + ); + + const result = await make.connectedSystems.create(ORGANIZATION_ID, body); + expect(result).toStrictEqual(connectedSystemCreateMock.connectedSystem); + }); + + it('Should update a connected system', async () => { + const body = { name: 'SAP staging' }; + + mockFetch( + `PATCH https://make.local/api/v2/connected-systems/${CONNECTED_SYSTEM_ID}?organizationId=${ORGANIZATION_ID}`, + connectedSystemUpdateMock, + req => { + expect(req.body).toStrictEqual(body); + }, + ); + + const result = await make.connectedSystems.update(ORGANIZATION_ID, CONNECTED_SYSTEM_ID, body); + expect(result).toStrictEqual(connectedSystemUpdateMock.connectedSystem); + }); + + it('Should delete a connected system', async () => { + mockFetch( + `DELETE https://make.local/api/v2/connected-systems/${CONNECTED_SYSTEM_ID}?organizationId=${ORGANIZATION_ID}`, + connectedSystemDeleteMock, + ); + + const result = await make.connectedSystems.delete(ORGANIZATION_ID, CONNECTED_SYSTEM_ID); + expect(result).toStrictEqual(connectedSystemDeleteMock.connectedSystem); + }); + + it('Should throw MakeError when the connected system is not found', async () => { + mockFetch( + `GET https://make.local/api/v2/connected-systems/${CONNECTED_SYSTEM_ID}?organizationId=${ORGANIZATION_ID}`, + { message: 'Not found' }, + 404, + ); + + await expect(make.connectedSystems.get(ORGANIZATION_ID, CONNECTED_SYSTEM_ID)).rejects.toMatchObject({ + name: 'MakeError', + statusCode: 404, + message: 'Not found', + }); + }); + + it('Should throw MakeError when agency rejects create', async () => { + const body = { + name: 'SAP production', + agentId: AGENT_ID, + appName: 'sap-agent', + inputs: { language: 'EN' }, + }; + + mockFetch( + `POST https://make.local/api/v2/connected-systems?organizationId=${ORGANIZATION_ID}`, + { message: 'Request to the Agency has failed: [400] Bad Request' }, + 400, + ); + + await expect(make.connectedSystems.create(ORGANIZATION_ID, body)).rejects.toBeInstanceOf(MakeError); + }); +}); diff --git a/test/enums.integration.test.ts b/test/enums.integration.test.ts index fe1dd95..1e5afe4 100644 --- a/test/enums.integration.test.ts +++ b/test/enums.integration.test.ts @@ -53,4 +53,17 @@ describe('Integration: Enums', () => { expect(timezone?.code).toBeDefined(); expect(timezone?.offset).toBeDefined(); }); + + it('Should list connected-system apps', async () => { + const apps = await make.enums.connectedSystemApps(); + + expect(apps).toBeDefined(); + expect(Array.isArray(apps)).toBe(true); + expect(apps.length).toBeGreaterThan(0); + + const app = apps[0]; + expect(app?.name).toBeDefined(); + expect(app?.label).toBeDefined(); + expect(app?.icon).toBeDefined(); + }); }); diff --git a/test/enums.spec.ts b/test/enums.spec.ts index fce176c..a9c8b8c 100644 --- a/test/enums.spec.ts +++ b/test/enums.spec.ts @@ -5,6 +5,7 @@ import { mockFetch } from './test.utils.js'; import * as countriesMock from './mocks/enums/countries.json'; import * as regionsMock from './mocks/enums/regions.json'; import * as timezonesMock from './mocks/enums/timezones.json'; +import * as connectedSystemAppsMock from './mocks/enums/connected-system-apps.json'; const MAKE_API_KEY = 'api-key'; const MAKE_ZONE = 'make.local'; @@ -32,4 +33,11 @@ describe('Endpoints: Enums', () => { const result = await make.enums.timezones(); expect(result).toStrictEqual(timezonesMock.timezones); }); + + it('Should list connected-system apps', async () => { + mockFetch('GET https://make.local/api/v2/enums/connected-system-apps', connectedSystemAppsMock); + + const result = await make.enums.connectedSystemApps(); + expect(result).toStrictEqual(connectedSystemAppsMock.connectedSystemApps); + }); }); diff --git a/test/make.spec.ts b/test/make.spec.ts index 78e6409..2f88db3 100644 --- a/test/make.spec.ts +++ b/test/make.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from '@jest/globals'; import { Make } from '../src/make.js'; +import { VERSION } from '../src/version.js'; import { mockFetch, TestableMake } from './test.utils.js'; import { randomUUID } from 'node:crypto'; import type { QueryValue } from '../src/types.js'; @@ -79,7 +80,7 @@ describe('Make SDK', () => { expect(url).toBe('/users/me'); expect(options?.headers).toStrictEqual({ authorization: `Token ${MAKE_API_KEY}`, - 'user-agent': 'MakeTypeScriptSDK/development', + 'user-agent': `MakeTypeScriptSDK/${VERSION}`, 'x-custom-header': 'test', }); return new Response('{"authUser": {"id": 1}}', { diff --git a/test/mcp.spec.ts b/test/mcp.spec.ts index c7db570..b96acaa 100644 --- a/test/mcp.spec.ts +++ b/test/mcp.spec.ts @@ -92,6 +92,30 @@ describe('MCP Tools', () => { expect(coreCategories).toContain('connections'); expect(coreCategories).toContain('teams'); expect(coreCategories).toContain('data-stores'); + expect(coreCategories).toContain('on-prem-agent'); + expect(coreCategories).toContain('connected-system'); + }); + + it('Should expose on-prem agent MCP tools', () => { + const onPremAgentTools = MakeTools.filter(tool => tool.category === 'on-prem-agent'); + const names = onPremAgentTools.map(tool => tool.name); + expect(names).toContain('on-prem-agent_list'); + expect(names).toContain('on-prem-agent_get'); + expect(names).toContain('on-prem-agent_create'); + expect(names).toContain('on-prem-agent_update'); + expect(names).toContain('on-prem-agent_delete'); + expect(names).toContain('on-prem-agent_get-app-config'); + }); + + it('Should expose connected-system MCP tools', () => { + const connectedSystemTools = MakeTools.filter(tool => tool.category === 'connected-system'); + const names = connectedSystemTools.map(tool => tool.name); + expect(names).toContain('connected-system_list'); + expect(names).toContain('connected-system_get'); + expect(names).toContain('connected-system_create'); + expect(names).toContain('connected-system_update'); + expect(names).toContain('connected-system_delete'); + expect(names).toContain('connected-system_list-apps'); }); it('Should have proper naming conventions', () => { diff --git a/test/mocks/agents/app-config.json b/test/mocks/agents/app-config.json new file mode 100644 index 0000000..c59139b --- /dev/null +++ b/test/mocks/agents/app-config.json @@ -0,0 +1,27 @@ +{ + "inputs": [ + { + "name": "inputs", + "label": "Inputs", + "type": "collection", + "spec": [ + { + "name": "ashost", + "label": "Application server host", + "required": true + }, + { + "name": "sysnr", + "label": "System number", + "required": true + }, + { + "name": "client", + "label": "Client", + "help": "SAP client number", + "required": true + } + ] + } + ] +} diff --git a/test/mocks/agents/create.json b/test/mocks/agents/create.json new file mode 100644 index 0000000..07cc52e --- /dev/null +++ b/test/mocks/agents/create.json @@ -0,0 +1,11 @@ +{ + "agent": { + "id": "c3d4e5f6-a7b8-9012-cdef-123456789012", + "tenantId": "tenant-1", + "name": "New bridge", + "clientSecret": "secret-shown-once", + "status": "REGISTERED", + "alerted": false, + "connected": false + } +} diff --git a/test/mocks/agents/delete.json b/test/mocks/agents/delete.json new file mode 100644 index 0000000..09098ca --- /dev/null +++ b/test/mocks/agents/delete.json @@ -0,0 +1,3 @@ +{ + "agent": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" +} diff --git a/test/mocks/agents/get.json b/test/mocks/agents/get.json new file mode 100644 index 0000000..426025b --- /dev/null +++ b/test/mocks/agents/get.json @@ -0,0 +1,12 @@ +{ + "agent": { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "tenantId": "tenant-1", + "name": "Production bridge", + "status": "ACTIVE", + "alerted": false, + "connected": true, + "version": "1.2.3", + "systemConnectionsCount": 2 + } +} diff --git a/test/mocks/agents/list.json b/test/mocks/agents/list.json new file mode 100644 index 0000000..ecd7ae0 --- /dev/null +++ b/test/mocks/agents/list.json @@ -0,0 +1,14 @@ +{ + "agents": [ + { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "tenantId": "tenant-1", + "name": "Production bridge", + "status": "ACTIVE", + "alerted": false, + "connected": true, + "version": "1.2.3", + "systemConnectionsCount": 2 + } + ] +} diff --git a/test/mocks/agents/update.json b/test/mocks/agents/update.json new file mode 100644 index 0000000..39d763e --- /dev/null +++ b/test/mocks/agents/update.json @@ -0,0 +1,10 @@ +{ + "agent": { + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "tenantId": "tenant-1", + "name": "Renamed bridge", + "status": "ACTIVE", + "alerted": false, + "connected": true + } +} diff --git a/test/mocks/connected-systems/create.json b/test/mocks/connected-systems/create.json new file mode 100644 index 0000000..130de28 --- /dev/null +++ b/test/mocks/connected-systems/create.json @@ -0,0 +1,13 @@ +{ + "connectedSystem": { + "id": "d4e5f6a7-b8c9-0123-defa-234567890123", + "name": "SAP production", + "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "appName": "sap-agent", + "inputs": [ + { "name": "ashost", "value": "00" }, + { "name": "sysnr", "value": "00" }, + { "name": "client", "value": "00" } + ] + } +} diff --git a/test/mocks/connected-systems/delete.json b/test/mocks/connected-systems/delete.json new file mode 100644 index 0000000..2f36e09 --- /dev/null +++ b/test/mocks/connected-systems/delete.json @@ -0,0 +1,3 @@ +{ + "connectedSystem": "b2c3d4e5-f6a7-8901-bcde-f12345678901" +} diff --git a/test/mocks/connected-systems/get.json b/test/mocks/connected-systems/get.json new file mode 100644 index 0000000..34d8538 --- /dev/null +++ b/test/mocks/connected-systems/get.json @@ -0,0 +1,13 @@ +{ + "connectedSystem": { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "name": "SAP production", + "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "appName": "sap-agent", + "inputs": [ + { "name": "ashost", "value": "00" }, + { "name": "sysnr", "value": "00" }, + { "name": "client", "value": "00" } + ] + } +} diff --git a/test/mocks/connected-systems/list.json b/test/mocks/connected-systems/list.json new file mode 100644 index 0000000..de52c5f --- /dev/null +++ b/test/mocks/connected-systems/list.json @@ -0,0 +1,15 @@ +{ + "connectedSystems": [ + { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "name": "SAP production", + "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "appName": "sap-agent", + "inputs": [ + { "name": "ashost", "value": "00" }, + { "name": "sysnr", "value": "00" }, + { "name": "client", "value": "00" } + ] + } + ] +} diff --git a/test/mocks/connected-systems/update.json b/test/mocks/connected-systems/update.json new file mode 100644 index 0000000..c562973 --- /dev/null +++ b/test/mocks/connected-systems/update.json @@ -0,0 +1,8 @@ +{ + "connectedSystem": { + "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "name": "SAP staging", + "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "appName": "sap-agent" + } +} diff --git a/test/mocks/enums/connected-system-apps.json b/test/mocks/enums/connected-system-apps.json new file mode 100644 index 0000000..d914e39 --- /dev/null +++ b/test/mocks/enums/connected-system-apps.json @@ -0,0 +1,14 @@ +{ + "connectedSystemApps": [ + { + "name": "http", + "label": "HTTP", + "icon": "http" + }, + { + "name": "sap-agent", + "label": "SAP", + "icon": "sap" + } + ] +} diff --git a/test/on-prem-connected-system.utils.spec.ts b/test/on-prem-connected-system.utils.spec.ts new file mode 100644 index 0000000..7e7a190 --- /dev/null +++ b/test/on-prem-connected-system.utils.spec.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from '@jest/globals'; +import type { AgentAppConfigInput } from '../src/endpoints/agents.js'; +import { + assertInputsMatchAppConfig, + inputFieldNamesFromAppConfig, + parseInputs, +} from './on-prem-connected-system.utils.js'; + +const SAP_APP_CONFIG: AgentAppConfigInput[] = [ + { + name: 'inputs', + label: 'Inputs', + type: 'collection', + spec: [ + { name: 'ashost', label: 'Host', required: true }, + { name: 'sysnr', label: 'System number', required: true }, + { name: 'client', label: 'Client', required: true }, + ], + }, +]; + +describe('on-prem connected-system integration utils', () => { + describe('parseInputs', () => { + it('Should return empty object when raw is undefined', () => { + expect(parseInputs(undefined, 'MAKE_CONNECTED_SYSTEM_HTTP_INPUTS')).toStrictEqual({}); + }); + + it('Should coerce values to strings', () => { + expect(parseInputs('{"port":8080}', 'ENV')).toStrictEqual({ port: '8080' }); + }); + + it('Should reject non-object JSON', () => { + expect(() => parseInputs('[]', 'ENV')).toThrow('ENV must be a JSON object'); + }); + }); + + describe('inputFieldNamesFromAppConfig', () => { + it('Should extract required and all field names from collection spec', () => { + expect(inputFieldNamesFromAppConfig(SAP_APP_CONFIG)).toStrictEqual({ + required: ['ashost', 'sysnr', 'client'], + all: ['ashost', 'sysnr', 'client'], + }); + }); + }); + + describe('assertInputsMatchAppConfig', () => { + it('Should pass when inputs match the form spec', () => { + expect(() => + assertInputsMatchAppConfig( + SAP_APP_CONFIG, + { ashost: '00', sysnr: '00', client: '00' }, + 'MAKE_CONNECTED_SYSTEM_SAP_AGENT_INPUTS', + 'sap-agent', + ), + ).not.toThrow(); + }); + + it('Should fail when required keys are missing', () => { + expect(() => + assertInputsMatchAppConfig( + SAP_APP_CONFIG, + { client: '00' }, + 'MAKE_CONNECTED_SYSTEM_SAP_AGENT_INPUTS', + 'sap-agent', + ), + ).toThrow(/Missing required keys: ashost, sysnr/); + }); + + it('Should fail when unknown keys are present', () => { + expect(() => + assertInputsMatchAppConfig( + SAP_APP_CONFIG, + { ashost: '00', sysnr: '00', client: '00', language: 'EN' }, + 'MAKE_CONNECTED_SYSTEM_SAP_AGENT_INPUTS', + 'sap-agent', + ), + ).toThrow(/Unknown keys \(not in form spec\): language/); + }); + }); +}); diff --git a/test/on-prem-connected-system.utils.ts b/test/on-prem-connected-system.utils.ts new file mode 100644 index 0000000..4cf0111 --- /dev/null +++ b/test/on-prem-connected-system.utils.ts @@ -0,0 +1,52 @@ +import type { AgentAppConfigInput } from '../src/endpoints/agents.js'; + +export function parseInputs(raw: string | undefined, envName: string): Record { + if (!raw) { + return {}; + } + const parsed: unknown = JSON.parse(raw); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error(`${envName} must be a JSON object`); + } + return Object.fromEntries( + Object.entries(parsed as Record).map(([key, value]) => [key, String(value)]), + ); +} + +export function inputFieldNamesFromAppConfig(config: AgentAppConfigInput[]): { + required: string[]; + all: string[]; +} { + const spec = config.find(input => input.name === 'inputs')?.spec ?? []; + + return { + required: spec.filter(field => field.required).map(field => field.name), + all: spec.map(field => field.name), + }; +} + +export function assertInputsMatchAppConfig( + config: AgentAppConfigInput[], + inputs: Record, + envName: string, + appName: string, +): void { + const { required, all } = inputFieldNamesFromAppConfig(config); + const provided = Object.keys(inputs); + const missing = required.filter(name => !provided.includes(name)); + const unknown = provided.filter(name => !all.includes(name)); + + if (missing.length > 0 || unknown.length > 0) { + const parts: string[] = [ + `${envName} does not match getAppConfig for ${appName} on this agent.`, + ]; + if (missing.length > 0) { + parts.push(`Missing required keys: ${missing.join(', ')}.`); + } + if (unknown.length > 0) { + parts.push(`Unknown keys (not in form spec): ${unknown.join(', ')}.`); + } + parts.push(`Expected field names: ${all.join(', ') || '(none)'}.`); + throw new Error(parts.join(' ')); + } +} diff --git a/test/on-prem-tools.spec.ts b/test/on-prem-tools.spec.ts new file mode 100644 index 0000000..d598233 --- /dev/null +++ b/test/on-prem-tools.spec.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from '@jest/globals'; +import { Make } from '../src/make.js'; +import { MakeTools } from '../src/tools.js'; +import { mockFetch } from './test.utils.js'; + +import * as agentsListMock from './mocks/agents/list.json'; +import * as agentAppConfigMock from './mocks/agents/app-config.json'; +import * as connectedSystemAppsMock from './mocks/enums/connected-system-apps.json'; +import * as connectedSystemsListMock from './mocks/connected-systems/list.json'; + +const MAKE_API_KEY = 'api-key'; +const MAKE_ZONE = 'make.local'; +const ORGANIZATION_ID = 5; +const AGENT_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; + +function getTool(name: string) { + const tool = MakeTools.find(entry => entry.name === name); + if (!tool) { + throw new Error(`Missing MCP tool: ${name}`); + } + return tool; +} + +describe('MCP tools: on-prem agent and connected-system', () => { + const make = new Make(MAKE_API_KEY, MAKE_ZONE); + + it('Should execute on-prem-agent_list', async () => { + mockFetch(`GET https://make.local/api/v2/agents?organizationId=${ORGANIZATION_ID}`, agentsListMock); + + const tool = getTool('on-prem-agent_list'); + const result = await tool.execute(make, { organizationId: ORGANIZATION_ID }); + + expect(result).toStrictEqual(agentsListMock.agents); + }); + + it('Should execute on-prem-agent_get-app-config', async () => { + mockFetch( + `GET https://make.local/api/v2/agents/${AGENT_ID}/apps/sap-agent/config?organizationId=${ORGANIZATION_ID}`, + agentAppConfigMock, + ); + + const tool = getTool('on-prem-agent_get-app-config'); + const result = await tool.execute(make, { + organizationId: ORGANIZATION_ID, + agentId: AGENT_ID, + appName: 'sap-agent', + }); + + expect(result).toStrictEqual(agentAppConfigMock.inputs); + }); + + it('Should execute connected-system_list-apps', async () => { + mockFetch( + 'GET https://make.local/api/v2/enums/connected-system-apps', + connectedSystemAppsMock, + ); + + const tool = getTool('connected-system_list-apps'); + const result = await tool.execute(make, {}); + + expect(result).toStrictEqual(connectedSystemAppsMock.connectedSystemApps); + }); + + it('Should execute connected-system_list', async () => { + mockFetch( + `GET https://make.local/api/v2/connected-systems?organizationId=${ORGANIZATION_ID}&agentId=${AGENT_ID}`, + connectedSystemsListMock, + ); + + const tool = getTool('connected-system_list'); + const result = await tool.execute(make, { + organizationId: ORGANIZATION_ID, + agentId: AGENT_ID, + }); + + expect(result).toStrictEqual(connectedSystemsListMock.connectedSystems); + }); +}); diff --git a/test/utils.spec.ts b/test/utils.spec.ts index 360ada2..80a0c49 100644 --- a/test/utils.spec.ts +++ b/test/utils.spec.ts @@ -19,6 +19,12 @@ describe('Utils', () => { ); }); + it('Should skip undefined array elements', () => { + expect(buildUrl('https://example.com', { tags: ['a', undefined, 'b'] })).toBe( + 'https://example.com?tags%5B%5D=a&tags%5B%5D=b', + ); + }); + it('Should build URL with nested parameters', () => { expect( buildUrl('https://example.com', {