@stage-labs/metro 0.1.0-beta.110 → 0.1.0-beta.111
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/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +3 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +31 -0
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/model.ts +9 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-client.ts +7 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +28 -6
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +20 -6
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth.ts +36 -8
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +7 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/verify.ts +1 -0
- package/runtime/runtime.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.111",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.111",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, the standing rules loaded at each session start, and a refresh skill for when the daemon could not keep the server list current. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -93,6 +93,7 @@ function connectorPayload(
|
|
|
93
93
|
transport: row.transport,
|
|
94
94
|
auth: row.auth,
|
|
95
95
|
header: row.header,
|
|
96
|
+
clientId: row.client?.clientId ?? null,
|
|
96
97
|
signIn: row.signIn,
|
|
97
98
|
verified: detail ? { ...summary, catalog } : summary,
|
|
98
99
|
};
|
|
@@ -134,6 +135,8 @@ async function handleCreate(
|
|
|
134
135
|
url: bodyField(body, 'url'),
|
|
135
136
|
header: bodyField(body, 'header'),
|
|
136
137
|
value: bodyField(body, 'value'),
|
|
138
|
+
clientId: bodyField(body, 'clientId'),
|
|
139
|
+
clientSecret: bodyField(body, 'clientSecret'),
|
|
137
140
|
});
|
|
138
141
|
log.info(
|
|
139
142
|
{ id: created.id, name: created.name, host: hostOf(created.url) },
|
|
@@ -5,11 +5,14 @@ import type {
|
|
|
5
5
|
VerifiedServer,
|
|
6
6
|
} from './verify.js';
|
|
7
7
|
import { readStoredTools } from './tools.js';
|
|
8
|
+
import type { OAuthClient } from './oauth-client.js';
|
|
8
9
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
9
10
|
|
|
10
11
|
const CONNECTOR_NAME_MAX = 64;
|
|
11
12
|
const HEADER_NAME_RE = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
|
|
12
13
|
const HEADER_VALUE_RE = /^[\x20-\x7e]{1,4096}$/;
|
|
14
|
+
const CLIENT_ID_RE = /^[\x21-\x7e]{1,256}$/;
|
|
15
|
+
const CLIENT_SECRET_RE = /^[\x21-\x7e]{1,1024}$/;
|
|
13
16
|
const DEFAULT_HEADER = 'Authorization';
|
|
14
17
|
|
|
15
18
|
export class ConnectorError extends ApiError {}
|
|
@@ -21,6 +24,7 @@ export interface ConnectorConfig {
|
|
|
21
24
|
createdAt: string;
|
|
22
25
|
verified: VerifiedRecord;
|
|
23
26
|
oauth: boolean;
|
|
27
|
+
client: OAuthClient | null;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
function text(value: unknown): string {
|
|
@@ -71,6 +75,30 @@ export function connectorAuth(
|
|
|
71
75
|
return { kind: 'header', name, value };
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
export function connectorClient(
|
|
79
|
+
rawId: unknown,
|
|
80
|
+
rawSecret: unknown,
|
|
81
|
+
): OAuthClient | null {
|
|
82
|
+
const clientId = text(rawId).trim();
|
|
83
|
+
const clientSecret = text(rawSecret).trim();
|
|
84
|
+
if (clientId === '' && clientSecret === '') return null;
|
|
85
|
+
if (clientId === '')
|
|
86
|
+
throw new ConnectorError('a client secret needs the client ID it belongs to', 400);
|
|
87
|
+
if (!CLIENT_ID_RE.test(clientId))
|
|
88
|
+
throw new ConnectorError('that is not a client ID Metro can send', 400);
|
|
89
|
+
if (clientSecret !== '' && !CLIENT_SECRET_RE.test(clientSecret))
|
|
90
|
+
throw new ConnectorError('that client secret is not sendable', 400);
|
|
91
|
+
return { clientId, ...(clientSecret === '' ? {} : { clientSecret }) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readClient(raw: unknown): OAuthClient | null {
|
|
95
|
+
if (!isRecord(raw)) return null;
|
|
96
|
+
const clientId = text(raw.clientId);
|
|
97
|
+
if (clientId === '') return null;
|
|
98
|
+
const clientSecret = text(raw.clientSecret);
|
|
99
|
+
return { clientId, ...(clientSecret === '' ? {} : { clientSecret }) };
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
function readOAuth(raw: Record<string, unknown>): ConnectorAuth {
|
|
75
103
|
const accessToken = text(raw.accessToken);
|
|
76
104
|
const clientId = text(raw.clientId);
|
|
@@ -79,6 +107,7 @@ function readOAuth(raw: Record<string, unknown>): ConnectorAuth {
|
|
|
79
107
|
return { kind: 'none' };
|
|
80
108
|
const refreshToken = text(raw.refreshToken);
|
|
81
109
|
const clientSecret = text(raw.clientSecret);
|
|
110
|
+
const scope = text(raw.scope);
|
|
82
111
|
return {
|
|
83
112
|
kind: 'oauth',
|
|
84
113
|
accessToken,
|
|
@@ -87,6 +116,7 @@ function readOAuth(raw: Record<string, unknown>): ConnectorAuth {
|
|
|
87
116
|
issuer: text(raw.issuer),
|
|
88
117
|
...(refreshToken === '' ? {} : { refreshToken }),
|
|
89
118
|
...(clientSecret === '' ? {} : { clientSecret }),
|
|
119
|
+
...(scope === '' ? {} : { scope }),
|
|
90
120
|
...(typeof raw.expiresAt === 'number' ? { expiresAt: raw.expiresAt } : {}),
|
|
91
121
|
};
|
|
92
122
|
}
|
|
@@ -123,6 +153,7 @@ export function readConfig(raw: unknown): ConnectorConfig {
|
|
|
123
153
|
createdAt: text(record.createdAt),
|
|
124
154
|
verified: readVerified(record.verified),
|
|
125
155
|
oauth: record.oauth === true || auth.kind === 'oauth',
|
|
156
|
+
client: readClient(record.client),
|
|
126
157
|
};
|
|
127
158
|
}
|
|
128
159
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ConnectorAuth, VerifiedRecord } from './verify.js';
|
|
2
|
+
import type { OAuthClient } from './oauth-client.js';
|
|
2
3
|
import { readConfig, signInState, type ConnectorSignIn } from './config.js';
|
|
3
4
|
import type { ConnectorTransport } from '@metro-labs/core/station-names';
|
|
4
5
|
|
|
@@ -14,11 +15,17 @@ export interface Connector {
|
|
|
14
15
|
expiresAt: number | null;
|
|
15
16
|
signIn: ConnectorSignIn;
|
|
16
17
|
verified: VerifiedRecord;
|
|
18
|
+
client: OAuthClient | null;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export interface
|
|
21
|
+
export interface PendingConnectorInput {
|
|
20
22
|
name: unknown;
|
|
21
23
|
url: unknown;
|
|
24
|
+
clientId: unknown;
|
|
25
|
+
clientSecret: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ConnectorInput extends PendingConnectorInput {
|
|
22
29
|
header: unknown;
|
|
23
30
|
value: unknown;
|
|
24
31
|
}
|
|
@@ -55,6 +62,7 @@ export function connectorFromRow(row: ConnectorLike): Connector {
|
|
|
55
62
|
expiresAt: auth.kind === 'oauth' ? (auth.expiresAt ?? null) : null,
|
|
56
63
|
signIn: signInState(config),
|
|
57
64
|
verified: config.verified,
|
|
65
|
+
client: config.client,
|
|
58
66
|
};
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -32,7 +32,9 @@ export async function registerClient(
|
|
|
32
32
|
): Promise<OAuthClient> {
|
|
33
33
|
if (server.registrationEndpoint === null)
|
|
34
34
|
throw refused(
|
|
35
|
-
'That server
|
|
35
|
+
'That server signs in with OAuth but registers no clients on its own. ' +
|
|
36
|
+
`Register an app with it yourself, with ${redirectUri} as its redirect URL, ` +
|
|
37
|
+
"and add the connector again with that app's client ID.",
|
|
36
38
|
);
|
|
37
39
|
let res: Response;
|
|
38
40
|
try {
|
|
@@ -80,6 +82,8 @@ export function authorizeUrl(input: AuthorizeInput): string {
|
|
|
80
82
|
url.searchParams.set('code_challenge', challengeOf(input.verifier));
|
|
81
83
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
82
84
|
url.searchParams.set('resource', input.resource);
|
|
85
|
+
if (input.server.scopes.length > 0)
|
|
86
|
+
url.searchParams.set('scope', input.server.scopes.join(' '));
|
|
83
87
|
return url.toString();
|
|
84
88
|
}
|
|
85
89
|
|
|
@@ -150,10 +154,12 @@ export function refreshTokens(
|
|
|
150
154
|
client: OAuthClient,
|
|
151
155
|
refreshToken: string,
|
|
152
156
|
resource: string,
|
|
157
|
+
scope?: string,
|
|
153
158
|
): Promise<OAuthTokens> {
|
|
154
159
|
return postToken(server, client, {
|
|
155
160
|
grant_type: 'refresh_token',
|
|
156
161
|
refresh_token: refreshToken,
|
|
157
162
|
resource,
|
|
163
|
+
...(scope === undefined ? {} : { scope }),
|
|
158
164
|
});
|
|
159
165
|
}
|
|
@@ -9,6 +9,12 @@ export interface OAuthServer {
|
|
|
9
9
|
tokenEndpoint: string;
|
|
10
10
|
registrationEndpoint: string | null;
|
|
11
11
|
supportsS256: boolean;
|
|
12
|
+
scopes: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ResourceMetadata {
|
|
16
|
+
issuer: URL;
|
|
17
|
+
scopes: string[];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
function refused(message: string): ConnectorVerifyError {
|
|
@@ -65,18 +71,33 @@ export async function advertisesOAuth(resource: URL): Promise<boolean> {
|
|
|
65
71
|
return false;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
function scopesOf(body: Record<string, unknown> | null): string[] {
|
|
75
|
+
const raw: unknown = body?.scopes_supported;
|
|
76
|
+
if (!Array.isArray(raw)) return [];
|
|
77
|
+
const out: string[] = [];
|
|
78
|
+
for (const entry of raw) {
|
|
79
|
+
const scope = str(entry).trim();
|
|
80
|
+
if (scope !== '' && !/\s/.test(scope) && !out.includes(scope)) out.push(scope);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function resourceMetadata(resource: URL): Promise<ResourceMetadata> {
|
|
69
86
|
for (const candidate of resourceMetadataUrls(resource)) {
|
|
70
87
|
const body = await getJson(candidate);
|
|
71
88
|
const servers: unknown = body?.authorization_servers;
|
|
72
89
|
const first: unknown = Array.isArray(servers) ? servers[0] : undefined;
|
|
73
90
|
if (typeof first === 'string' && first !== '')
|
|
74
|
-
return parseConnectorUrl(first);
|
|
91
|
+
return { issuer: parseConnectorUrl(first), scopes: scopesOf(body) };
|
|
75
92
|
}
|
|
76
|
-
return parseConnectorUrl(resource.origin);
|
|
93
|
+
return { issuer: parseConnectorUrl(resource.origin), scopes: [] };
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
function toServer(
|
|
96
|
+
function toServer(
|
|
97
|
+
body: Record<string, unknown>,
|
|
98
|
+
issuer: URL,
|
|
99
|
+
scopes: string[],
|
|
100
|
+
): OAuthServer {
|
|
80
101
|
const authorizationEndpoint = str(body.authorization_endpoint);
|
|
81
102
|
const tokenEndpoint = str(body.token_endpoint);
|
|
82
103
|
if (authorizationEndpoint === '' || tokenEndpoint === '')
|
|
@@ -91,6 +112,7 @@ function toServer(body: Record<string, unknown>, issuer: URL): OAuthServer {
|
|
|
91
112
|
tokenEndpoint,
|
|
92
113
|
registrationEndpoint: registration === '' ? null : registration,
|
|
93
114
|
supportsS256: Array.isArray(methods) ? methods.includes('S256') : false,
|
|
115
|
+
scopes,
|
|
94
116
|
};
|
|
95
117
|
}
|
|
96
118
|
|
|
@@ -106,10 +128,10 @@ export function authServerMetadataUrls(issuer: URL): URL[] {
|
|
|
106
128
|
}
|
|
107
129
|
|
|
108
130
|
export async function discoverOAuth(resource: URL): Promise<OAuthServer> {
|
|
109
|
-
const issuer = await
|
|
131
|
+
const { issuer, scopes } = await resourceMetadata(resource);
|
|
110
132
|
for (const candidate of authServerMetadataUrls(issuer)) {
|
|
111
133
|
const body = await getJson(candidate);
|
|
112
|
-
if (body !== null) return toServer(body, issuer);
|
|
134
|
+
if (body !== null) return toServer(body, issuer, scopes);
|
|
113
135
|
}
|
|
114
136
|
throw refused(
|
|
115
137
|
`${resource.hostname} needs authorization, but publishes no OAuth metadata Metro can use.`,
|
|
@@ -4,12 +4,14 @@ import { bodyField, readJsonBody, sendJson, type ApiSession } from '@metro-labs/
|
|
|
4
4
|
import {
|
|
5
5
|
beginOAuth,
|
|
6
6
|
completeOAuth,
|
|
7
|
+
prepareOAuth,
|
|
7
8
|
takePending,
|
|
8
9
|
type PendingAuth,
|
|
9
10
|
} from './oauth.js';
|
|
11
|
+
import { connectorClient } from './config.js';
|
|
10
12
|
import { parseConnectorUrl } from './verify.js';
|
|
11
13
|
import type { OAuthAuth } from './verify.js';
|
|
12
|
-
import type { Connector } from './model.js';
|
|
14
|
+
import type { Connector, PendingConnectorInput } from './model.js';
|
|
13
15
|
|
|
14
16
|
const asText = (value: unknown): string =>
|
|
15
17
|
typeof value === 'string' ? value : '';
|
|
@@ -26,7 +28,7 @@ export interface OAuthRouteDeps {
|
|
|
26
28
|
createPendingConnector: (
|
|
27
29
|
subject: string,
|
|
28
30
|
project: string,
|
|
29
|
-
input:
|
|
31
|
+
input: PendingConnectorInput,
|
|
30
32
|
) => Promise<Connector>;
|
|
31
33
|
reconnectConnector: (
|
|
32
34
|
subject: string,
|
|
@@ -46,15 +48,25 @@ export async function startOAuth(
|
|
|
46
48
|
payload: (row: Connector) => Record<string, unknown>,
|
|
47
49
|
): Promise<void> {
|
|
48
50
|
const url = parseConnectorUrl(bodyField(body, 'url'));
|
|
51
|
+
const returnTo = asText(bodyField(body, 'returnTo'));
|
|
52
|
+
const clientId = bodyField(body, 'clientId');
|
|
53
|
+
const clientSecret = bodyField(body, 'clientSecret');
|
|
54
|
+
const prepared = await prepareOAuth({
|
|
55
|
+
url,
|
|
56
|
+
client: connectorClient(clientId, clientSecret),
|
|
57
|
+
returnTo,
|
|
58
|
+
});
|
|
49
59
|
const row = await deps.createPendingConnector(session.subject, project, {
|
|
50
60
|
name: bodyField(body, 'name'),
|
|
51
61
|
url: bodyField(body, 'url'),
|
|
62
|
+
clientId,
|
|
63
|
+
clientSecret,
|
|
52
64
|
});
|
|
53
|
-
const authorize =
|
|
65
|
+
const authorize = beginOAuth(prepared, {
|
|
54
66
|
subject: session.subject,
|
|
55
67
|
name: row.name,
|
|
56
68
|
url,
|
|
57
|
-
returnTo
|
|
69
|
+
returnTo,
|
|
58
70
|
connectorId: row.id,
|
|
59
71
|
});
|
|
60
72
|
log.info(
|
|
@@ -74,11 +86,13 @@ export async function handleConnect(
|
|
|
74
86
|
const row = await deps.getConnector(session.subject, id);
|
|
75
87
|
const url = parseConnectorUrl(row.url);
|
|
76
88
|
const body = await readJsonBody(req);
|
|
77
|
-
const
|
|
89
|
+
const returnTo = asText(bodyField(body, 'returnTo'));
|
|
90
|
+
const prepared = await prepareOAuth({ url, client: row.client, returnTo });
|
|
91
|
+
const authorize = beginOAuth(prepared, {
|
|
78
92
|
subject: session.subject,
|
|
79
93
|
name: row.name,
|
|
80
94
|
url,
|
|
81
|
-
returnTo
|
|
95
|
+
returnTo,
|
|
82
96
|
connectorId: row.id,
|
|
83
97
|
});
|
|
84
98
|
log.info(
|
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
newVerifier,
|
|
12
12
|
refreshTokens,
|
|
13
13
|
registerClient,
|
|
14
|
+
type OAuthClient,
|
|
14
15
|
} from './oauth-client.js';
|
|
15
|
-
import { discoverOAuth } from './oauth-discovery.js';
|
|
16
|
+
import { discoverOAuth, type OAuthServer } from './oauth-discovery.js';
|
|
16
17
|
import { startPending, takePending, type PendingAuth } from './oauth-pending.js';
|
|
17
18
|
|
|
18
19
|
export { takePending, type PendingAuth };
|
|
@@ -30,20 +31,37 @@ function refused(message: string): ConnectorVerifyError {
|
|
|
30
31
|
|
|
31
32
|
const resourceOf = (url: URL): string => url.toString();
|
|
32
33
|
|
|
33
|
-
export interface
|
|
34
|
-
connectorId: string;
|
|
35
|
-
subject: string;
|
|
36
|
-
name: string;
|
|
34
|
+
export interface PrepareInput {
|
|
37
35
|
url: URL;
|
|
36
|
+
client: OAuthClient | null;
|
|
38
37
|
returnTo: string;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
export
|
|
40
|
+
export interface PreparedOAuth {
|
|
41
|
+
server: OAuthServer;
|
|
42
|
+
client: OAuthClient;
|
|
43
|
+
redirectUri: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function prepareOAuth(input: PrepareInput): Promise<PreparedOAuth> {
|
|
42
47
|
if (!validateReturnTo(input.returnTo))
|
|
43
48
|
throw refused('that return address is not one Metro will send you back to');
|
|
44
49
|
const server = await discoverOAuth(input.url);
|
|
45
50
|
const redirectUri = callbackUri();
|
|
46
|
-
const client = await registerClient(server, redirectUri);
|
|
51
|
+
const client = input.client ?? (await registerClient(server, redirectUri));
|
|
52
|
+
return { server, client, redirectUri };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BeginInput {
|
|
56
|
+
connectorId: string;
|
|
57
|
+
subject: string;
|
|
58
|
+
name: string;
|
|
59
|
+
url: URL;
|
|
60
|
+
returnTo: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function beginOAuth(prepared: PreparedOAuth, input: BeginInput): string {
|
|
64
|
+
const { server, client, redirectUri } = prepared;
|
|
47
65
|
const verifier = newVerifier();
|
|
48
66
|
const resource = resourceOf(input.url);
|
|
49
67
|
const state = startPending({
|
|
@@ -76,6 +94,9 @@ function authOf(pendingAuth: PendingAuth, tokens: OAuthTokens): OAuthAuth {
|
|
|
76
94
|
...(pendingAuth.client.clientSecret === undefined
|
|
77
95
|
? {}
|
|
78
96
|
: { clientSecret: pendingAuth.client.clientSecret }),
|
|
97
|
+
...(pendingAuth.server.scopes.length === 0
|
|
98
|
+
? {}
|
|
99
|
+
: { scope: pendingAuth.server.scopes.join(' ') }),
|
|
79
100
|
...tokens,
|
|
80
101
|
};
|
|
81
102
|
}
|
|
@@ -109,6 +130,7 @@ export async function refreshOAuth(
|
|
|
109
130
|
tokenEndpoint: auth.tokenEndpoint,
|
|
110
131
|
registrationEndpoint: null,
|
|
111
132
|
supportsS256: true,
|
|
133
|
+
scopes: [],
|
|
112
134
|
};
|
|
113
135
|
const client = {
|
|
114
136
|
clientId: auth.clientId,
|
|
@@ -116,7 +138,13 @@ export async function refreshOAuth(
|
|
|
116
138
|
? {}
|
|
117
139
|
: { clientSecret: auth.clientSecret }),
|
|
118
140
|
};
|
|
119
|
-
const tokens = await refreshTokens(
|
|
141
|
+
const tokens = await refreshTokens(
|
|
142
|
+
server,
|
|
143
|
+
client,
|
|
144
|
+
auth.refreshToken,
|
|
145
|
+
resource,
|
|
146
|
+
auth.scope,
|
|
147
|
+
);
|
|
120
148
|
return {
|
|
121
149
|
...auth,
|
|
122
150
|
...tokens,
|
|
@@ -14,6 +14,7 @@ import { advertisesOAuth } from './oauth-discovery.js';
|
|
|
14
14
|
import {
|
|
15
15
|
ConnectorError,
|
|
16
16
|
connectorAuth,
|
|
17
|
+
connectorClient,
|
|
17
18
|
connectorName,
|
|
18
19
|
readConfig,
|
|
19
20
|
stamp,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
type ConnectorCheck,
|
|
33
34
|
type ConnectorInput,
|
|
34
35
|
type DeletedConnector,
|
|
36
|
+
type PendingConnectorInput,
|
|
35
37
|
} from './model.js';
|
|
36
38
|
import { agentsDir } from '../agents/files.js';
|
|
37
39
|
import { assertLocalOwner, LOCAL_PROJECT_ID } from '../agents/file-admin.js';
|
|
@@ -155,26 +157,29 @@ export async function localCreateConnector(
|
|
|
155
157
|
const name = connectorName(input.name);
|
|
156
158
|
const url = parseConnectorUrl(input.url);
|
|
157
159
|
const auth = connectorAuth(input.header, input.value);
|
|
160
|
+
const client = connectorClient(input.clientId, input.clientSecret);
|
|
158
161
|
const verified = stamp(await verifyRemoteMcp(url, auth));
|
|
159
162
|
return insert(dir, name, url, {
|
|
160
163
|
auth,
|
|
161
164
|
createdAt: new Date().toISOString(),
|
|
162
165
|
verified,
|
|
163
166
|
oauth: await oauthCapable(url, auth),
|
|
167
|
+
client,
|
|
164
168
|
});
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
export async function localCreatePendingConnector(
|
|
168
172
|
subject: string,
|
|
169
173
|
project: string,
|
|
170
|
-
input:
|
|
174
|
+
input: PendingConnectorInput,
|
|
171
175
|
dir = agentsDir(),
|
|
172
176
|
): Promise<Connector> {
|
|
173
177
|
ownedRows(subject, project, dir);
|
|
174
178
|
const name = connectorName(input.name);
|
|
175
179
|
const url = parseConnectorUrl(input.url);
|
|
180
|
+
const client = connectorClient(input.clientId, input.clientSecret);
|
|
176
181
|
return Promise.resolve(
|
|
177
|
-
insert(dir, name, url, { auth: { kind: 'none' }, createdAt: new Date().toISOString(), verified: UNVERIFIED, oauth: true }),
|
|
182
|
+
insert(dir, name, url, { auth: { kind: 'none' }, createdAt: new Date().toISOString(), verified: UNVERIFIED, oauth: true, client }),
|
|
178
183
|
);
|
|
179
184
|
}
|
|
180
185
|
|
package/runtime/runtime.json
CHANGED