Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions integrations/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,37 @@ export interface OAuthProviderRegistrationDefaults {
toolDescription?: string;
requestMethod?: string;
requestPath?: string;
/**
* Provider-specific, HTTP-status-keyed error hints surfaced to the user when
* a managed connector call fails (e.g. HubSpot 401/403 reconnection advice).
* Keeps provider knowledge out of the hub's own source.
*/
errorHints?: Record<number, string>;
/**
* Declarative managed-connector migration descriptor. Lets the integrations
* hub detect and normalize legacy stored connectors from catalog data instead
* of branching on a provider slug in its own source.
*/
managedConnectorMigration?: ManagedConnectorMigration;
}

/**
* Describes how to migrate a legacy stored managed connector to the canonical
* shape declared on the provider's catalog entry.
*/
export interface ManagedConnectorMigration {
/** Canonical MCP/HTTP server URL the connector should resolve to. */
canonicalServerUrl: string;
/**
* Historical OAuth scope bundles. A stored connector whose scopes match one
* of these bundles is treated as legacy and forced to re-discover tools.
*/
legacyScopeBundles: {
required: string[];
optional: string[];
union: string[];
unionWithoutOauth: string[];
};
Comment on lines +142 to +147

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Four fields here where two would carry the same information. union is always [...required, ...optional] and unionWithoutOauth is union.filter(s => s !== 'oauth') — the hub can compute both from required + optional. Declaring all four as data creates a hidden invariant (changing required must imply recomputing union, otherwise the hub's match logic drifts). Either drop union / unionWithoutOauth and let the consumer derive them, or keep just union + an oauthScope: string field.

Suggested change
legacyScopeBundles: {
required: string[];
optional: string[];
union: string[];
unionWithoutOauth: string[];
};
legacyScopeBundles: {
required: string[];
optional: string[];
};

}

export interface OAuthProviderCatalogOption {
Expand Down Expand Up @@ -165,5 +196,8 @@ export const hubspotMcpAuthorizationUrl: string;
export const hubspotMcpTokenUrl: string;
export const hubspotRequiredScopes: readonly string[];
export const hubspotOptionalScopes: readonly string[];
export const hubspotLegacyScopeBundle: readonly string[];
export const hubspotLegacyScopeBundleWithoutOauth: readonly string[];
export const hubspotManagedConnectorMigration: ManagedConnectorMigration;

export default INTEGRATION_CATALOG;
3 changes: 3 additions & 0 deletions integrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import { listOAuthProviderCatalog } from "./oauth-provider-catalog.js";
export { listOAuthProviderCatalog } from "./oauth-provider-catalog.js";
export {
getOAuthProviderRegistrationDefaults,
hubspotLegacyScopeBundle,
hubspotLegacyScopeBundleWithoutOauth,
hubspotManagedConnectorMigration,
hubspotMcpAuthorizationUrl,
hubspotMcpServerUrl,
hubspotMcpTokenUrl,
Expand Down
37 changes: 37 additions & 0 deletions integrations/oauth-provider-registration-defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,36 @@ export const hubspotOptionalScopes = [
"crm.schemas.deals.read",
];

/**
* Historical HubSpot OAuth scope bundles. Before the managed connector pointed
* at mcp.hubspot.com, already-stored HubSpot connectors carried these scopes.
* The integrations hub compares a stored connector's scopes against these
* bundles to detect legacy connectors that need re-discovery, so the values
* here must match the bundles the hub previously hardcoded exactly.
*/
Comment on lines +90 to +96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: This 7-line block comment narrates the diff and explains what the hub does with the data — neither survives in the source. The data's meaning is already in the ManagedConnectorMigration interface JSDoc in index.d.ts. Trim to a single line:

Suggested change
/**
* Historical HubSpot OAuth scope bundles. Before the managed connector pointed
* at mcp.hubspot.com, already-stored HubSpot connectors carried these scopes.
* The integrations hub compares a stored connector's scopes against these
* bundles to detect legacy connectors that need re-discovery, so the values
* here must match the bundles the hub previously hardcoded exactly.
*/
// Scope bundles the hub matches against stored connectors to detect legacy ones.

export const hubspotLegacyScopeBundle = [
...hubspotRequiredScopes,
...hubspotOptionalScopes,
];

export const hubspotLegacyScopeBundleWithoutOauth =
hubspotLegacyScopeBundle.filter((scope) => scope !== "oauth");

/**
* Declarative HubSpot managed-connector migration descriptor. Lets the
* integrations hub migrate/normalize legacy HubSpot connectors from catalog
* data instead of branching on the "hubspot" slug in its own source.
*/
Comment on lines +105 to +109

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: This 5-line block comment restates what index.d.ts already says about ManagedConnectorMigration. Drop it — the exported constant name + its type are self-describing.

Suggested change
/**
* Declarative HubSpot managed-connector migration descriptor. Lets the
* integrations hub migrate/normalize legacy HubSpot connectors from catalog
* data instead of branching on the "hubspot" slug in its own source.
*/

export const hubspotManagedConnectorMigration = {
canonicalServerUrl: hubspotMcpServerUrl,
legacyScopeBundles: {
required: hubspotRequiredScopes,
optional: hubspotOptionalScopes,
union: hubspotLegacyScopeBundle,
unionWithoutOauth: hubspotLegacyScopeBundleWithoutOauth,
},
};

const registrationDefaults = {
github: {
apiBaseUrl: "https://api.github.com",
Expand Down Expand Up @@ -330,6 +360,13 @@ const registrationDefaults = {
scopes: [],
credentialHelp:
"Use the client ID and secret from a HubSpot MCP auth app (Development → MCP Auth Apps). Standard HubSpot OAuth apps and private apps will not authenticate with mcp.hubspot.com.",
managedConnectorMigration: hubspotManagedConnectorMigration,
Comment thread
neubig marked this conversation as resolved.
errorHints: {
Comment thread
neubig marked this conversation as resolved.
401:
"HubSpot MCP requires a user-level OAuth token from a HubSpot MCP auth app. Reconnect HubSpot with an MCP auth app instead of a standard HubSpot OAuth app or private app.",
403:
"HubSpot MCP may need reauthorization to grant the current server tool scopes. Disconnect and reconnect HubSpot, then try discovery again.",
},
},
zendesk: {
apiBaseUrl: "https://{subdomain}.zendesk.com/api/v2",
Expand Down
2 changes: 1 addition & 1 deletion skills/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion skills/openhands-sdk/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ Source: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/
- [`40_acp_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/40_acp_agent_example.py)
- [`41_task_tool_set.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/41_task_tool_set.py)
- [`42_file_based_subagents.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/42_file_based_subagents.py)
- [`43_mixed_marketplace_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/43_mixed_marketplace_skills)
- [`44_model_switching_in_convo.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/44_model_switching_in_convo.py)
- [`45_parallel_tool_execution.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/45_parallel_tool_execution.py)
- [`46_agent_settings.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/46_agent_settings.py)
Expand Down Expand Up @@ -232,4 +231,5 @@ Source: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/
- [`01_loading_agentskills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/01_loading_agentskills)
- [`02_loading_plugins`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/02_loading_plugins)
- [`03_managing_installed_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/03_managing_installed_skills)
- [`04_mixed_marketplace_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/04_mixed_marketplace_skills)

58 changes: 58 additions & 0 deletions tests/test_catalogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,61 @@ def test_node_package_exports_catalogs():
if (!AUTOMATION_CATALOG.some((entry) => entry.id === 'github-pr-reviewer')) process.exit(1);
"""
subprocess.run(["node", "--input-type=module", "-e", script], cwd=ROOT, check=True)


def test_hubspot_managed_connector_migration_is_declared():
"""The HubSpot catalog entry must declare the managed-connector migration
descriptor (canonicalServerUrl + the four legacy scope bundles) so the
integrations hub can migrate legacy HubSpot connectors from catalog data
instead of branching on the "hubspot" slug. The bundles must match the
historical hub constants exactly."""
script = r"""
import {
getOAuthProviderRegistrationDefaults,
hubspotMcpServerUrl,
hubspotRequiredScopes,
hubspotOptionalScopes,
} from './integrations/index.js';

const defaults = getOAuthProviderRegistrationDefaults('hubspot');
const migration = defaults?.managedConnectorMigration;
if (!migration) process.exit(1);

const expectedUnion = [...hubspotRequiredScopes, ...hubspotOptionalScopes];
const expectedUnionWithoutOauth = expectedUnion.filter((s) => s !== 'oauth');

const assertEqual = (actual, expected, label) => {
const a = JSON.stringify(actual);
const e = JSON.stringify(expected);
if (a !== e) {
console.error(`${label} mismatch:\n actual: ${a}\n expected: ${e}`);
process.exit(1);
}
};

if (migration.canonicalServerUrl !== hubspotMcpServerUrl) process.exit(1);
if (migration.canonicalServerUrl !== 'https://mcp.hubspot.com') process.exit(1);

assertEqual(migration.legacyScopeBundles.required, hubspotRequiredScopes, 'required');
assertEqual(migration.legacyScopeBundles.optional, hubspotOptionalScopes, 'optional');
assertEqual(migration.legacyScopeBundles.union, expectedUnion, 'union');
assertEqual(
migration.legacyScopeBundles.unionWithoutOauth,
expectedUnionWithoutOauth,
'unionWithoutOauth',
);

// errorHints must remain declared alongside the migration metadata.
if (!defaults.errorHints?.[401] || !defaults.errorHints?.[403]) process.exit(1);
// Canonical OAuth config the hub reads from the same entry.
if (
defaults.provider !== 'mcp' ||
defaults.serverUrl !== hubspotMcpServerUrl ||
defaults.clientAuthentication !== 'body' ||
defaults.pkce !== true ||
defaults.scopes.length !== 0
) {
process.exit(1);
}
"""
subprocess.run(["node", "--input-type=module", "-e", script], cwd=ROOT, check=True)
Loading