@topolo/mcp 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/dispatch.js +5 -5
- package/dist/http.js +5 -5
- package/dist/index.js +383 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ yet. GET requests work without confirmation; mutating HTTP methods must pass
|
|
|
123
123
|
`confirm: true` or the SDK rejects the call. Backing services still enforce
|
|
124
124
|
permissions for every request. The tool schema intentionally does not embed a
|
|
125
125
|
large service enum; agents should call `topolo_list_services` first, then pass a
|
|
126
|
-
|
|
126
|
+
app ID/slug from that credential-scoped result. New services become callable
|
|
127
127
|
without an MCP release once Auth grants the organization and credential the
|
|
128
128
|
relevant permissions, and new actions become typed tools without an MCP release
|
|
129
129
|
once the application publishes them.
|
package/dist/dispatch.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
APPLICATION_REQUIREMENTS,
|
|
7
7
|
APPLICATION_REQUIREMENTS_VERSION,
|
|
8
8
|
APPLICATIONS,
|
|
9
|
-
|
|
9
|
+
DEFAULT_APP_URLS,
|
|
10
10
|
auditAllApplicationRequirements,
|
|
11
11
|
applicationRequirementScopes,
|
|
12
12
|
requirementsForApplication,
|
|
@@ -257,7 +257,7 @@ var TOOLS = [
|
|
|
257
257
|
{
|
|
258
258
|
name: "topolo_api_call",
|
|
259
259
|
title: "Call any Topolo platform service",
|
|
260
|
-
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_list_services first to discover callable
|
|
260
|
+
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_list_services first to discover callable app IDs; the SDK can also resolve live Auth-catalog app ids/slugs for newly registered API surfaces.",
|
|
261
261
|
requiredScopes: [],
|
|
262
262
|
destructive: true,
|
|
263
263
|
inputSchema: {
|
|
@@ -265,7 +265,7 @@ var TOOLS = [
|
|
|
265
265
|
properties: {
|
|
266
266
|
service: {
|
|
267
267
|
type: "string",
|
|
268
|
-
description: "Target
|
|
268
|
+
description: "Target app ID from the platform registry."
|
|
269
269
|
},
|
|
270
270
|
method: {
|
|
271
271
|
type: "string",
|
|
@@ -370,7 +370,7 @@ function actionsToToolDefs(actions) {
|
|
|
370
370
|
}));
|
|
371
371
|
}
|
|
372
372
|
function toolNameForAction(action) {
|
|
373
|
-
const service = (action.
|
|
373
|
+
const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
374
374
|
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
375
375
|
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
376
376
|
}
|
|
@@ -435,7 +435,7 @@ function displayAppId(entry) {
|
|
|
435
435
|
for (const value of [entry.slug, entry.name, entry.appId]) {
|
|
436
436
|
if (typeof value !== "string" || !value.trim()) continue;
|
|
437
437
|
for (const candidate of appIdCandidates(value)) {
|
|
438
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
438
|
+
if (Object.prototype.hasOwnProperty.call(DEFAULT_APP_URLS, candidate)) {
|
|
439
439
|
return candidate;
|
|
440
440
|
}
|
|
441
441
|
}
|
package/dist/http.js
CHANGED
|
@@ -15814,7 +15814,7 @@ import {
|
|
|
15814
15814
|
APPLICATION_REQUIREMENTS,
|
|
15815
15815
|
APPLICATION_REQUIREMENTS_VERSION,
|
|
15816
15816
|
APPLICATIONS,
|
|
15817
|
-
|
|
15817
|
+
DEFAULT_APP_URLS,
|
|
15818
15818
|
auditAllApplicationRequirements,
|
|
15819
15819
|
applicationRequirementScopes,
|
|
15820
15820
|
requirementsForApplication,
|
|
@@ -16065,7 +16065,7 @@ var TOOLS = [
|
|
|
16065
16065
|
{
|
|
16066
16066
|
name: "topolo_api_call",
|
|
16067
16067
|
title: "Call any Topolo platform service",
|
|
16068
|
-
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_list_services first to discover callable
|
|
16068
|
+
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_list_services first to discover callable app IDs; the SDK can also resolve live Auth-catalog app ids/slugs for newly registered API surfaces.",
|
|
16069
16069
|
requiredScopes: [],
|
|
16070
16070
|
destructive: true,
|
|
16071
16071
|
inputSchema: {
|
|
@@ -16073,7 +16073,7 @@ var TOOLS = [
|
|
|
16073
16073
|
properties: {
|
|
16074
16074
|
service: {
|
|
16075
16075
|
type: "string",
|
|
16076
|
-
description: "Target
|
|
16076
|
+
description: "Target app ID from the platform registry."
|
|
16077
16077
|
},
|
|
16078
16078
|
method: {
|
|
16079
16079
|
type: "string",
|
|
@@ -16178,7 +16178,7 @@ function actionsToToolDefs(actions) {
|
|
|
16178
16178
|
}));
|
|
16179
16179
|
}
|
|
16180
16180
|
function toolNameForAction(action) {
|
|
16181
|
-
const service = (action.
|
|
16181
|
+
const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
16182
16182
|
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
16183
16183
|
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
16184
16184
|
}
|
|
@@ -16243,7 +16243,7 @@ function displayAppId(entry) {
|
|
|
16243
16243
|
for (const value of [entry.slug, entry.name, entry.appId]) {
|
|
16244
16244
|
if (typeof value !== "string" || !value.trim()) continue;
|
|
16245
16245
|
for (const candidate of appIdCandidates(value)) {
|
|
16246
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
16246
|
+
if (Object.prototype.hasOwnProperty.call(DEFAULT_APP_URLS, candidate)) {
|
|
16247
16247
|
return candidate;
|
|
16248
16248
|
}
|
|
16249
16249
|
}
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
58
58
|
this.path = path;
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
|
-
var
|
|
61
|
+
var APP_API_URLS = {
|
|
62
62
|
agent: "https://agent.topolo.app",
|
|
63
63
|
auth: "https://auth.topolo.app",
|
|
64
64
|
backup: "https://backup.topolo.app",
|
|
@@ -1645,12 +1645,12 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1645
1645
|
{
|
|
1646
1646
|
id: "shared-auth-boundary",
|
|
1647
1647
|
title: "Use the shared Topolo auth boundary",
|
|
1648
|
-
summary: "Applications must derive identity, organization, scopes, and
|
|
1648
|
+
summary: "Applications must derive identity, organization, scopes, and app entitlement from TopoloAuth rather than inventing parallel auth state.",
|
|
1649
1649
|
appliesTo: ["browser", "api", "tooling", "agent_surface"],
|
|
1650
1650
|
evidence: [
|
|
1651
1651
|
"@topolo/auth-client or @topolo-io/worker-runtime is used where applicable",
|
|
1652
1652
|
"No public command, tool, or SDK method accepts orgId",
|
|
1653
|
-
"
|
|
1653
|
+
"App slug and Auth app registration are documented for protected APIs"
|
|
1654
1654
|
],
|
|
1655
1655
|
implementation: [
|
|
1656
1656
|
"Use shared auth/runtime packages instead of copying token parsing, gateway, or browser storage flows",
|
|
@@ -1683,20 +1683,20 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1683
1683
|
]
|
|
1684
1684
|
},
|
|
1685
1685
|
{
|
|
1686
|
-
id: "
|
|
1687
|
-
title: "Register protected APIs as platform
|
|
1688
|
-
summary: "Callable app APIs need a stable
|
|
1686
|
+
id: "app-registration-and-scopes",
|
|
1687
|
+
title: "Register protected APIs as platform apps",
|
|
1688
|
+
summary: "Callable app APIs need a stable app ID, app slug, permissions, role bundles, and API-key scopes before agents or users rely on them.",
|
|
1689
1689
|
appliesTo: ["api"],
|
|
1690
1690
|
evidence: [
|
|
1691
|
-
"Auth
|
|
1692
|
-
"Permissions, role bundles, and API-key scopes exist for the
|
|
1693
|
-
"SDK, CLI, and MCP
|
|
1694
|
-
"CloudControl production HTTP targets classify agent reachability with
|
|
1691
|
+
"Auth app catalog contains the app ID and slug",
|
|
1692
|
+
"Permissions, role bundles, and API-key scopes exist for the app",
|
|
1693
|
+
"SDK, CLI, and MCP app discovery include the callable production API",
|
|
1694
|
+
"CloudControl production HTTP targets classify agent reachability with sdk_app_id or agent_callable:false"
|
|
1695
1695
|
],
|
|
1696
1696
|
implementation: [
|
|
1697
|
-
"Add production Worker api_url metadata and
|
|
1697
|
+
"Add production Worker api_url metadata and sdk_app_id for every agent-callable API target",
|
|
1698
1698
|
"Mark browser, documentation, and runtime-only Worker targets with agent_callable:false",
|
|
1699
|
-
"Expose stable
|
|
1699
|
+
"Expose stable app IDs through generated DEFAULT_APP_URLS and live catalog discovery",
|
|
1700
1700
|
"Keep typed SDK/CLI/MCP surfaces in sync when an API contract stabilizes"
|
|
1701
1701
|
]
|
|
1702
1702
|
},
|
|
@@ -1902,7 +1902,7 @@ function evaluateRequirement(application, requirement) {
|
|
|
1902
1902
|
"Catalog metadata cannot verify shared shell, launcher, and account menu adoption.",
|
|
1903
1903
|
evidence
|
|
1904
1904
|
);
|
|
1905
|
-
case "
|
|
1905
|
+
case "app-registration-and-scopes": {
|
|
1906
1906
|
if (application.services.length > 0) {
|
|
1907
1907
|
evidence.push(`services=${application.services.join(",")}`);
|
|
1908
1908
|
return finding(
|
|
@@ -1912,7 +1912,7 @@ function evaluateRequirement(application, requirement) {
|
|
|
1912
1912
|
evidence
|
|
1913
1913
|
);
|
|
1914
1914
|
}
|
|
1915
|
-
return finding(requirement, "missing", "No SDK
|
|
1915
|
+
return finding(requirement, "missing", "No SDK app API id is registered for this callable API surface.", evidence);
|
|
1916
1916
|
}
|
|
1917
1917
|
case "organization-scoped-data":
|
|
1918
1918
|
return finding(
|
|
@@ -1997,7 +1997,7 @@ function statusRank(status) {
|
|
|
1997
1997
|
if (status === "partial") return 1;
|
|
1998
1998
|
return 2;
|
|
1999
1999
|
}
|
|
2000
|
-
var
|
|
2000
|
+
var DEFAULT_APP_URLS = APP_API_URLS;
|
|
2001
2001
|
function resolveCatalogServiceUrl(entry, options = {}) {
|
|
2002
2002
|
const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
|
|
2003
2003
|
const override = resolveServiceUrlOverride(keys, options.overrides);
|
|
@@ -2009,7 +2009,7 @@ function resolveCatalogServiceUrl(entry, options = {}) {
|
|
|
2009
2009
|
function resolveServiceUrl(service, overrides) {
|
|
2010
2010
|
const override = resolveServiceUrlOverride([service], overrides);
|
|
2011
2011
|
if (override) return override;
|
|
2012
|
-
return
|
|
2012
|
+
return DEFAULT_APP_URLS[service];
|
|
2013
2013
|
}
|
|
2014
2014
|
function catalogServiceKeyCandidates(entry, preferred) {
|
|
2015
2015
|
const values = [preferred, entry.slug, entry.name, entry.appId].filter(
|
|
@@ -2040,7 +2040,7 @@ function resolveServiceUrlOverride(keys, overrides) {
|
|
|
2040
2040
|
if (envStyle) return envStyle;
|
|
2041
2041
|
}
|
|
2042
2042
|
for (const key of keys) {
|
|
2043
|
-
const envKey = `
|
|
2043
|
+
const envKey = `TOPOLO_APP_URL_${key.replace(/\./g, "_").toUpperCase()}`;
|
|
2044
2044
|
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2045
2045
|
if (envValue) return envValue;
|
|
2046
2046
|
}
|
|
@@ -2225,7 +2225,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2225
2225
|
const derived = deriveDevelopersFromAuth(resolveConfiguredServiceUrl("auth", this.serviceUrls));
|
|
2226
2226
|
if (derived) return { serviceKey, baseUrl: derived };
|
|
2227
2227
|
}
|
|
2228
|
-
return { serviceKey, baseUrl:
|
|
2228
|
+
return { serviceKey, baseUrl: DEFAULT_APP_URLS[serviceKey] };
|
|
2229
2229
|
}
|
|
2230
2230
|
const live = await this.resolveLiveCallableService(serviceKey);
|
|
2231
2231
|
if (live) {
|
|
@@ -2234,7 +2234,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2234
2234
|
}
|
|
2235
2235
|
throw new TopoloSdkError(
|
|
2236
2236
|
"unknown_service",
|
|
2237
|
-
`Unknown Topolo
|
|
2237
|
+
`Unknown Topolo app API "${service}". Run app discovery first and pass a callable app id or slug.`
|
|
2238
2238
|
);
|
|
2239
2239
|
}
|
|
2240
2240
|
async resolveLiveCallableService(serviceKey) {
|
|
@@ -2407,7 +2407,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2407
2407
|
async callAction(action, input = {}, options = {}) {
|
|
2408
2408
|
const entry = await this.getAction(action);
|
|
2409
2409
|
const mapped = mapActionInput(entry, input);
|
|
2410
|
-
const service = entry.
|
|
2410
|
+
const service = entry.appSlug || entry.appId;
|
|
2411
2411
|
return this.request({
|
|
2412
2412
|
service,
|
|
2413
2413
|
method: entry.method,
|
|
@@ -2595,8 +2595,8 @@ function normalizeActionCatalogEntry(item) {
|
|
|
2595
2595
|
title: pickActionString(a.title) ?? name,
|
|
2596
2596
|
description: pickActionString(a.description) ?? "",
|
|
2597
2597
|
appId,
|
|
2598
|
-
|
|
2599
|
-
|
|
2598
|
+
appSlug: pickActionString(a.app_slug) ?? pickActionString(a.appSlug) ?? null,
|
|
2599
|
+
appName: pickActionString(a.app_name) ?? pickActionString(a.appName) ?? null,
|
|
2600
2600
|
method,
|
|
2601
2601
|
path,
|
|
2602
2602
|
permissionName: pickActionString(a.permission_name) ?? pickActionString(a.permissionName) ?? "",
|
|
@@ -2711,6 +2711,7 @@ var CrmModule = class {
|
|
|
2711
2711
|
constructor(client) {
|
|
2712
2712
|
this.client = client;
|
|
2713
2713
|
}
|
|
2714
|
+
client;
|
|
2714
2715
|
async listContacts(options = {}) {
|
|
2715
2716
|
const res = await this.client.request({
|
|
2716
2717
|
service: "crm",
|
|
@@ -2756,6 +2757,7 @@ var IdentityModule = class {
|
|
|
2756
2757
|
constructor(client) {
|
|
2757
2758
|
this.client = client;
|
|
2758
2759
|
}
|
|
2760
|
+
client;
|
|
2759
2761
|
/** Returns the resolved user, organization, and permission set for the
|
|
2760
2762
|
* current credential. Equivalent to `TopoloClient.introspect()` — exposed
|
|
2761
2763
|
* on this module for symmetry with the other domain modules. */
|
|
@@ -2772,22 +2774,353 @@ function createTopolo(options) {
|
|
|
2772
2774
|
};
|
|
2773
2775
|
}
|
|
2774
2776
|
|
|
2775
|
-
//
|
|
2776
|
-
|
|
2777
|
-
|
|
2777
|
+
// ../topolo-sdk/dist/node/credential-store.js
|
|
2778
|
+
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
2779
|
+
import { existsSync } from "fs";
|
|
2780
|
+
import { homedir, platform } from "os";
|
|
2781
|
+
import { dirname, join } from "path";
|
|
2782
|
+
var TopoloSdkError2 = class extends Error {
|
|
2783
|
+
code;
|
|
2784
|
+
constructor(code, message) {
|
|
2785
|
+
super(message);
|
|
2786
|
+
this.name = "TopoloSdkError";
|
|
2787
|
+
this.code = code;
|
|
2788
|
+
}
|
|
2789
|
+
};
|
|
2790
|
+
var TopoloAuthError2 = class extends TopoloSdkError2 {
|
|
2791
|
+
constructor(message, code = "auth_error") {
|
|
2792
|
+
super(code, message);
|
|
2793
|
+
this.name = "TopoloAuthError";
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2796
|
+
var TopoloHttpError2 = class extends TopoloSdkError2 {
|
|
2797
|
+
status;
|
|
2798
|
+
body;
|
|
2799
|
+
service;
|
|
2800
|
+
path;
|
|
2801
|
+
constructor(service, path, status, body, message) {
|
|
2802
|
+
super("http_error", message ?? `HTTP ${status} from ${service}${path}`);
|
|
2803
|
+
this.name = "TopoloHttpError";
|
|
2804
|
+
this.status = status;
|
|
2805
|
+
this.body = body;
|
|
2806
|
+
this.service = service;
|
|
2807
|
+
this.path = path;
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
var APP_API_URLS2 = {
|
|
2811
|
+
agent: "https://agent.topolo.app",
|
|
2812
|
+
auth: "https://auth.topolo.app",
|
|
2813
|
+
backup: "https://backup.topolo.app",
|
|
2814
|
+
books: "https://books.topolo.app",
|
|
2815
|
+
bugfix: "https://bugfix.topolo.app",
|
|
2816
|
+
bytes: "https://bytes.topolo.app",
|
|
2817
|
+
calendar: "https://calendar.topolo.app",
|
|
2818
|
+
capacity: "https://capacity.topolo.app",
|
|
2819
|
+
chat: "https://chat.topolo.app",
|
|
2820
|
+
commerce: "https://commerce.topolo.app",
|
|
2821
|
+
compose: "https://compose.topolo.app",
|
|
2822
|
+
consent: "https://consent.topolo.app",
|
|
2823
|
+
crm: "https://crm.topolo.app",
|
|
2824
|
+
developers: "https://developers.topolo.app",
|
|
2825
|
+
director: "https://director.topolo.app",
|
|
2826
|
+
feed: "https://feed.topolo.app",
|
|
2827
|
+
flow: "https://flow.topolo.app",
|
|
2828
|
+
forecast: "https://forecast.topolo.app",
|
|
2829
|
+
forms: "https://forms.topolo.app",
|
|
2830
|
+
home: "https://home.topolo.app",
|
|
2831
|
+
insights: "https://insights.topolo.app",
|
|
2832
|
+
inventory: "https://inventory.topolo.app",
|
|
2833
|
+
learn: "https://learn.topolo.app",
|
|
2834
|
+
localize: "https://localize.topolo.app",
|
|
2835
|
+
mail: "https://mail.topolo.app",
|
|
2836
|
+
mdm: "https://mdm.topolo.app",
|
|
2837
|
+
messages: "https://messages.topolo.app",
|
|
2838
|
+
nexus: "https://nexus.topolo.app",
|
|
2839
|
+
notify: "https://notify.topolo.app",
|
|
2840
|
+
observability_watch: "https://observability-watch.topolo.app",
|
|
2841
|
+
one: "https://www.topolo.app",
|
|
2842
|
+
p2p: "https://p2p.topolo.app",
|
|
2843
|
+
pay: "https://pay.topolo.app",
|
|
2844
|
+
people: "https://people.topolo.app",
|
|
2845
|
+
"quro.api": "https://api.ol0.me",
|
|
2846
|
+
"quro.redirect": "https://ol0.me",
|
|
2847
|
+
roadmapper: "https://roadmapper.topolo.app",
|
|
2848
|
+
sign: "https://sign.topolo.app",
|
|
2849
|
+
social_studio: "https://studio.topolo.app",
|
|
2850
|
+
socialize: "https://socialize.topolo.app",
|
|
2851
|
+
spaces: "https://spaces.topolo.app",
|
|
2852
|
+
status: "https://status.topolo.app",
|
|
2853
|
+
success: "https://success.topolo.app",
|
|
2854
|
+
support: "https://support.topolo.app",
|
|
2855
|
+
survey: "https://survey.topolo.app",
|
|
2856
|
+
voice: "https://voice.topolo.app",
|
|
2857
|
+
web: "https://web.topolo.app"
|
|
2858
|
+
};
|
|
2859
|
+
var DEFAULT_APP_URLS2 = APP_API_URLS2;
|
|
2860
|
+
function resolveServiceUrl2(service, overrides) {
|
|
2861
|
+
const override = resolveServiceUrlOverride2([service], overrides);
|
|
2862
|
+
if (override) return override;
|
|
2863
|
+
return DEFAULT_APP_URLS2[service];
|
|
2864
|
+
}
|
|
2865
|
+
function resolveServiceUrlOverride2(keys, overrides) {
|
|
2866
|
+
for (const key of keys) {
|
|
2867
|
+
const direct = overrides?.[key];
|
|
2868
|
+
if (direct) return direct;
|
|
2869
|
+
const envStyleKey = key.replace(/\./g, "_");
|
|
2870
|
+
const envStyle = overrides?.[envStyleKey];
|
|
2871
|
+
if (envStyle) return envStyle;
|
|
2872
|
+
}
|
|
2873
|
+
for (const key of keys) {
|
|
2874
|
+
const envKey = `TOPOLO_APP_URL_${key.replace(/\./g, "_").toUpperCase()}`;
|
|
2875
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2876
|
+
if (envValue) return envValue;
|
|
2877
|
+
}
|
|
2878
|
+
return null;
|
|
2879
|
+
}
|
|
2880
|
+
var DEVICE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
2881
|
+
var TopoloOAuth = class {
|
|
2882
|
+
baseUrl;
|
|
2883
|
+
fetchImpl;
|
|
2884
|
+
constructor(options = {}) {
|
|
2885
|
+
this.baseUrl = resolveServiceUrl2("auth", options.serviceUrls);
|
|
2886
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* RFC 8628 device authorization grant — starts the flow by asking the server
|
|
2890
|
+
* for a device_code + user_code. The caller prints the user_code +
|
|
2891
|
+
* verification_uri and then polls `pollDeviceToken` until approval.
|
|
2892
|
+
*/
|
|
2893
|
+
async requestDeviceCode(params) {
|
|
2894
|
+
const body = new URLSearchParams();
|
|
2895
|
+
body.set("client_id", params.clientId);
|
|
2896
|
+
if (params.scope) {
|
|
2897
|
+
body.set("scope", Array.isArray(params.scope) ? params.scope.join(" ") : params.scope);
|
|
2898
|
+
}
|
|
2899
|
+
return this.#postForm("/api/developer-oauth/device_authorization", body);
|
|
2900
|
+
}
|
|
2901
|
+
/**
|
|
2902
|
+
* Polls the token endpoint once. Returns the token pair on success, or
|
|
2903
|
+
* throws a `TopoloAuthError` whose `code` is one of the RFC 8628 poll
|
|
2904
|
+
* states: `authorization_pending`, `slow_down`, `access_denied`,
|
|
2905
|
+
* `expired_token`. Callers should back off on `slow_down`, wait at least
|
|
2906
|
+
* one `interval` on `authorization_pending`, and give up on the rest.
|
|
2907
|
+
*/
|
|
2908
|
+
async pollDeviceToken(params) {
|
|
2909
|
+
const body = new URLSearchParams();
|
|
2910
|
+
body.set("grant_type", DEVICE_GRANT);
|
|
2911
|
+
body.set("client_id", params.clientId);
|
|
2912
|
+
body.set("device_code", params.deviceCode);
|
|
2913
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
2914
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
2915
|
+
}
|
|
2916
|
+
/** Exchange an authorization code (with PKCE) for tokens. */
|
|
2917
|
+
async exchangeAuthorizationCode(params) {
|
|
2918
|
+
const body = new URLSearchParams();
|
|
2919
|
+
body.set("grant_type", "authorization_code");
|
|
2920
|
+
body.set("client_id", params.clientId);
|
|
2921
|
+
body.set("code", params.code);
|
|
2922
|
+
body.set("redirect_uri", params.redirectUri);
|
|
2923
|
+
if (params.codeVerifier) body.set("code_verifier", params.codeVerifier);
|
|
2924
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
2925
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
2926
|
+
}
|
|
2927
|
+
/** Rotate a refresh token for a new token pair. */
|
|
2928
|
+
async refreshToken(params) {
|
|
2929
|
+
const body = new URLSearchParams();
|
|
2930
|
+
body.set("grant_type", "refresh_token");
|
|
2931
|
+
body.set("client_id", params.clientId);
|
|
2932
|
+
body.set("refresh_token", params.refreshToken);
|
|
2933
|
+
if (params.clientSecret) body.set("client_secret", params.clientSecret);
|
|
2934
|
+
return this.#postForm("/api/developer-oauth/token", body);
|
|
2935
|
+
}
|
|
2936
|
+
async #postForm(path, body) {
|
|
2937
|
+
const url = new URL(path, this.baseUrl.endsWith("/") ? this.baseUrl : `${this.baseUrl}/`);
|
|
2938
|
+
const res = await this.fetchImpl(url.toString(), {
|
|
2939
|
+
method: "POST",
|
|
2940
|
+
headers: {
|
|
2941
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
2942
|
+
Accept: "application/json"
|
|
2943
|
+
},
|
|
2944
|
+
body: body.toString()
|
|
2945
|
+
});
|
|
2946
|
+
const contentType = res.headers.get("Content-Type") ?? "";
|
|
2947
|
+
const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
2948
|
+
if (!res.ok) {
|
|
2949
|
+
if (parsed && typeof parsed === "object" && "error" in parsed) {
|
|
2950
|
+
const err = parsed;
|
|
2951
|
+
throw new TopoloAuthError2(err.error_description || err.error, err.error);
|
|
2952
|
+
}
|
|
2953
|
+
throw new TopoloHttpError2("auth", path, res.status, parsed);
|
|
2954
|
+
}
|
|
2955
|
+
return parsed;
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
2959
|
+
production: {
|
|
2960
|
+
developers: "https://developers.topolo.app"
|
|
2961
|
+
},
|
|
2962
|
+
staging: {
|
|
2963
|
+
auth: "https://auth.stg.topolo.us",
|
|
2964
|
+
developers: "https://developers.stg.topolo.us",
|
|
2965
|
+
seed: "https://seed.stg.topolo.us"
|
|
2966
|
+
},
|
|
2967
|
+
development: {
|
|
2968
|
+
auth: "https://auth.topolo.dev",
|
|
2969
|
+
developers: "https://developers.topolo.dev"
|
|
2970
|
+
},
|
|
2971
|
+
local: {}
|
|
2972
|
+
};
|
|
2973
|
+
function configPath() {
|
|
2974
|
+
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
2975
|
+
if (override) return override;
|
|
2976
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join(homedir(), "AppData", "Roaming") : join(homedir(), ".config"));
|
|
2977
|
+
return join(base, "topolo", "config.json");
|
|
2978
|
+
}
|
|
2979
|
+
async function readConfig() {
|
|
2980
|
+
const path = configPath();
|
|
2981
|
+
if (!existsSync(path)) return {};
|
|
2982
|
+
try {
|
|
2983
|
+
const raw = await readFile(path, "utf8");
|
|
2984
|
+
return JSON.parse(raw);
|
|
2985
|
+
} catch {
|
|
2986
|
+
return {};
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
async function writeConfig(config) {
|
|
2990
|
+
const path = configPath();
|
|
2991
|
+
await mkdir(dirname(path), { recursive: true });
|
|
2992
|
+
await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
2993
|
+
if (platform() !== "win32") {
|
|
2994
|
+
try {
|
|
2995
|
+
await chmod(path, 384);
|
|
2996
|
+
} catch {
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function normalizeEnvironment(value) {
|
|
3001
|
+
const normalized = (value ?? "production").trim().toLowerCase();
|
|
3002
|
+
if (normalized === "" || normalized === "production" || normalized === "prod") {
|
|
3003
|
+
return "production";
|
|
3004
|
+
}
|
|
3005
|
+
if (normalized === "staging" || normalized === "stage" || normalized === "stg") {
|
|
3006
|
+
return "staging";
|
|
3007
|
+
}
|
|
3008
|
+
if (normalized === "development" || normalized === "dev") {
|
|
3009
|
+
return "development";
|
|
3010
|
+
}
|
|
3011
|
+
if (normalized === "local" || normalized === "localhost") {
|
|
3012
|
+
return "local";
|
|
3013
|
+
}
|
|
3014
|
+
throw new Error(
|
|
3015
|
+
`Unknown Topolo environment "${value}". Expected production, staging, development, or local.`
|
|
3016
|
+
);
|
|
3017
|
+
}
|
|
3018
|
+
function selectedEnvironment() {
|
|
3019
|
+
return normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
3020
|
+
}
|
|
3021
|
+
function envVarPrefix(environment) {
|
|
3022
|
+
if (environment === "production") return "TOPOLO_PRODUCTION";
|
|
3023
|
+
if (environment === "staging") return "TOPOLO_STAGING";
|
|
3024
|
+
if (environment === "development") return "TOPOLO_DEVELOPMENT";
|
|
3025
|
+
return "TOPOLO_LOCAL";
|
|
3026
|
+
}
|
|
3027
|
+
function profileForEnvironment(config, environment = selectedEnvironment()) {
|
|
3028
|
+
const profile = config.profiles?.[environment] ?? {};
|
|
3029
|
+
if (environment === "production") {
|
|
3030
|
+
return {
|
|
3031
|
+
credential: config.credential,
|
|
3032
|
+
oauthClientId: config.oauthClientId,
|
|
3033
|
+
...profile,
|
|
3034
|
+
serviceUrls: {
|
|
3035
|
+
...config.serviceUrls ?? {},
|
|
3036
|
+
...profile.serviceUrls ?? {}
|
|
3037
|
+
},
|
|
3038
|
+
agentName: profile.agentName ?? config.agentName
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
return profile;
|
|
3042
|
+
}
|
|
3043
|
+
function writeProfileConfig(config, environment, profile) {
|
|
3044
|
+
const profiles = { ...config.profiles ?? {} };
|
|
3045
|
+
profiles[environment] = {
|
|
3046
|
+
...profiles[environment] ?? {},
|
|
3047
|
+
...profile
|
|
3048
|
+
};
|
|
3049
|
+
return { ...config, profiles };
|
|
3050
|
+
}
|
|
3051
|
+
async function resolveCredential(environment = selectedEnvironment()) {
|
|
3052
|
+
const prefix = envVarPrefix(environment);
|
|
3053
|
+
const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
|
|
2778
3054
|
if (apiKey) return { kind: "api_key", apiKey };
|
|
2779
|
-
const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
|
|
3055
|
+
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
|
|
2780
3056
|
if (accessToken) return { kind: "access_token", accessToken };
|
|
2781
|
-
|
|
3057
|
+
const config = await readConfig();
|
|
3058
|
+
return profileForEnvironment(config, environment).credential ?? null;
|
|
3059
|
+
}
|
|
3060
|
+
var REFRESH_SKEW_SECONDS = 60;
|
|
3061
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
3062
|
+
const prefix = envVarPrefix(environment);
|
|
3063
|
+
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
3064
|
+
return resolveCredential(environment);
|
|
3065
|
+
}
|
|
3066
|
+
const config = await readConfig();
|
|
3067
|
+
const profile = profileForEnvironment(config, environment);
|
|
3068
|
+
const cred = profile.credential;
|
|
3069
|
+
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
3070
|
+
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
3071
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
3072
|
+
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
3073
|
+
try {
|
|
3074
|
+
const oauth = new TopoloOAuth({
|
|
3075
|
+
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
3076
|
+
});
|
|
3077
|
+
const tokens = await oauth.refreshToken({
|
|
3078
|
+
clientId: profile.oauthClientId,
|
|
3079
|
+
refreshToken: cred.refreshToken
|
|
3080
|
+
});
|
|
3081
|
+
const refreshed = {
|
|
3082
|
+
kind: "access_token",
|
|
3083
|
+
accessToken: tokens.access_token,
|
|
3084
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
3085
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
3086
|
+
};
|
|
3087
|
+
await writeConfig(
|
|
3088
|
+
environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
|
|
3089
|
+
);
|
|
3090
|
+
return refreshed;
|
|
3091
|
+
} catch {
|
|
3092
|
+
return cred;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
function resolveAgentName(configAgentName) {
|
|
3096
|
+
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
|
2782
3097
|
}
|
|
2783
|
-
function
|
|
2784
|
-
const
|
|
3098
|
+
function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
|
|
3099
|
+
const merged = {
|
|
3100
|
+
...ENVIRONMENT_DEFAULT_SERVICE_URLS[environment],
|
|
3101
|
+
...configUrls ?? {}
|
|
3102
|
+
};
|
|
3103
|
+
const prefix = `${envVarPrefix(environment)}_SERVICE_URL_`;
|
|
3104
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
3105
|
+
if (k.startsWith(prefix) && v) {
|
|
3106
|
+
merged[k.slice(prefix.length).toLowerCase()] = v;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
2785
3109
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2786
3110
|
if (k.startsWith("TOPOLO_SERVICE_URL_") && v) {
|
|
2787
|
-
|
|
3111
|
+
merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
|
|
2788
3112
|
}
|
|
2789
3113
|
}
|
|
2790
|
-
return Object.keys(
|
|
3114
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// src/auth.ts
|
|
3118
|
+
function resolveCredentialFromEnv() {
|
|
3119
|
+
const apiKey = process.env.TOPOLO_API_KEY;
|
|
3120
|
+
if (apiKey) return { kind: "api_key", apiKey };
|
|
3121
|
+
const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
|
|
3122
|
+
if (accessToken) return { kind: "access_token", accessToken };
|
|
3123
|
+
return null;
|
|
2791
3124
|
}
|
|
2792
3125
|
|
|
2793
3126
|
// src/server.ts
|
|
@@ -3041,7 +3374,7 @@ var TOOLS = [
|
|
|
3041
3374
|
{
|
|
3042
3375
|
name: "topolo_api_call",
|
|
3043
3376
|
title: "Call any Topolo platform service",
|
|
3044
|
-
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_list_services first to discover callable
|
|
3377
|
+
description: "Low-level passthrough to any Topolo service. Use when no domain-specific tool exists for the operation you need. The request is signed with the current credential and scoped to the caller's organization \u2014 the target service will return 403 if your scopes do not permit the operation, even though this tool is advertised. Mutating calls (non-GET methods) require confirm:true. Call topolo_list_services first to discover callable app IDs; the SDK can also resolve live Auth-catalog app ids/slugs for newly registered API surfaces.",
|
|
3045
3378
|
requiredScopes: [],
|
|
3046
3379
|
destructive: true,
|
|
3047
3380
|
inputSchema: {
|
|
@@ -3049,7 +3382,7 @@ var TOOLS = [
|
|
|
3049
3382
|
properties: {
|
|
3050
3383
|
service: {
|
|
3051
3384
|
type: "string",
|
|
3052
|
-
description: "Target
|
|
3385
|
+
description: "Target app ID from the platform registry."
|
|
3053
3386
|
},
|
|
3054
3387
|
method: {
|
|
3055
3388
|
type: "string",
|
|
@@ -3154,7 +3487,7 @@ function actionsToToolDefs(actions) {
|
|
|
3154
3487
|
}));
|
|
3155
3488
|
}
|
|
3156
3489
|
function toolNameForAction(action) {
|
|
3157
|
-
const service = (action.
|
|
3490
|
+
const service = (action.appSlug || action.appId || "service").replace(/^topolo-/, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3158
3491
|
const name = action.name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
3159
3492
|
return ["topolo", service, name].filter(Boolean).join("_").slice(0, 96);
|
|
3160
3493
|
}
|
|
@@ -3219,7 +3552,7 @@ function displayAppId(entry) {
|
|
|
3219
3552
|
for (const value of [entry.slug, entry.name, entry.appId]) {
|
|
3220
3553
|
if (typeof value !== "string" || !value.trim()) continue;
|
|
3221
3554
|
for (const candidate of appIdCandidates(value)) {
|
|
3222
|
-
if (Object.prototype.hasOwnProperty.call(
|
|
3555
|
+
if (Object.prototype.hasOwnProperty.call(DEFAULT_APP_URLS, candidate)) {
|
|
3223
3556
|
return candidate;
|
|
3224
3557
|
}
|
|
3225
3558
|
}
|
|
@@ -3245,14 +3578,14 @@ function formatKnownValues(values, label) {
|
|
|
3245
3578
|
|
|
3246
3579
|
// src/version.ts
|
|
3247
3580
|
import { readFileSync } from "fs";
|
|
3248
|
-
import { dirname, join } from "path";
|
|
3581
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
3249
3582
|
import { fileURLToPath } from "url";
|
|
3250
3583
|
function readMcpVersion() {
|
|
3251
3584
|
try {
|
|
3252
|
-
const here =
|
|
3585
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
3253
3586
|
const candidates = [
|
|
3254
|
-
|
|
3255
|
-
|
|
3587
|
+
join2(here, "package.json"),
|
|
3588
|
+
join2(here, "..", "package.json")
|
|
3256
3589
|
];
|
|
3257
3590
|
for (const path of candidates) {
|
|
3258
3591
|
try {
|
|
@@ -3331,15 +3664,21 @@ function toolError(message) {
|
|
|
3331
3664
|
|
|
3332
3665
|
// src/index.ts
|
|
3333
3666
|
async function main() {
|
|
3334
|
-
const
|
|
3667
|
+
const environment = normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
3668
|
+
let credential = resolveCredentialFromEnv();
|
|
3669
|
+
if (!credential) {
|
|
3670
|
+
credential = await refreshCredentialIfNeeded(environment);
|
|
3671
|
+
}
|
|
3335
3672
|
if (!credential) {
|
|
3336
3673
|
process.stderr.write(
|
|
3337
|
-
"TopoloMCP:
|
|
3674
|
+
"TopoloMCP: no Topolo credential. Run `topolo auth login`, or set TOPOLO_API_KEY / TOPOLO_ACCESS_TOKEN.\n"
|
|
3338
3675
|
);
|
|
3339
3676
|
process.exit(1);
|
|
3340
3677
|
}
|
|
3341
|
-
const
|
|
3342
|
-
const
|
|
3678
|
+
const config = await readConfig();
|
|
3679
|
+
const profile = profileForEnvironment(config, environment);
|
|
3680
|
+
const agentName = resolveAgentName(profile.agentName);
|
|
3681
|
+
const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
|
|
3343
3682
|
const topolo = createTopolo({
|
|
3344
3683
|
credential,
|
|
3345
3684
|
agent: {
|
|
@@ -3356,7 +3695,7 @@ async function main() {
|
|
|
3356
3695
|
await server.connect(transport);
|
|
3357
3696
|
const toolNames = await advertisedToolNames(topolo, scopes);
|
|
3358
3697
|
process.stderr.write(
|
|
3359
|
-
`TopoloMCP v${MCP_VERSION} connected. Tools available: ${toolNames.join(", ") || "(none)"}
|
|
3698
|
+
`TopoloMCP v${MCP_VERSION} connected (${environment}). Tools available: ${toolNames.join(", ") || "(none)"}
|
|
3360
3699
|
`
|
|
3361
3700
|
);
|
|
3362
3701
|
}
|
package/package.json
CHANGED