@ottocode/sdk 0.1.201 → 0.1.202
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/src/config/src/paths.ts +16 -14
- package/src/core/src/index.ts +1 -0
- package/src/core/src/mcp/index.ts +1 -0
- package/src/core/src/mcp/lifecycle.ts +58 -24
- package/src/core/src/mcp/oauth/store.ts +2 -8
- package/src/core/src/mcp/server-manager.ts +33 -5
- package/src/core/src/mcp/types.ts +3 -0
- package/src/index.ts +3 -0
package/package.json
CHANGED
package/src/config/src/paths.ts
CHANGED
|
@@ -35,29 +35,31 @@ export function getGlobalAuthPath(): string {
|
|
|
35
35
|
return joinPath(getGlobalConfigDir(), 'auth.json');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
// - Linux: $XDG_STATE_HOME/otto/auth.json or ~/.local/state/otto/auth.json
|
|
40
|
-
// - macOS: ~/Library/Application Support/otto/auth.json
|
|
41
|
-
// - Windows: %APPDATA%\otto\auth.json
|
|
42
|
-
export function getSecureAuthPath(): string {
|
|
38
|
+
export function getSecureBaseDir(): string {
|
|
43
39
|
const platform = process.platform;
|
|
44
40
|
if (platform === 'darwin') {
|
|
45
|
-
return joinPath(
|
|
46
|
-
getHomeDir(),
|
|
47
|
-
'Library',
|
|
48
|
-
'Application Support',
|
|
49
|
-
'otto',
|
|
50
|
-
'auth.json',
|
|
51
|
-
);
|
|
41
|
+
return joinPath(getHomeDir(), 'Library', 'Application Support', 'otto');
|
|
52
42
|
}
|
|
53
43
|
if (platform === 'win32') {
|
|
54
44
|
const appData = (process.env.APPDATA || '').replace(/\\/g, '/');
|
|
55
45
|
const base = appData || joinPath(getHomeDir(), 'AppData', 'Roaming');
|
|
56
|
-
return joinPath(base, 'otto'
|
|
46
|
+
return joinPath(base, 'otto');
|
|
57
47
|
}
|
|
58
48
|
const stateHome = (process.env.XDG_STATE_HOME || '').replace(/\\/g, '/');
|
|
59
49
|
const base = stateHome || joinPath(getHomeDir(), '.local', 'state');
|
|
60
|
-
return joinPath(base, 'otto'
|
|
50
|
+
return joinPath(base, 'otto');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function getSecureOAuthDir(): string {
|
|
54
|
+
return joinPath(getSecureBaseDir(), 'oauth');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Secure location for auth secrets (not in config dir or project)
|
|
58
|
+
// - Linux: $XDG_STATE_HOME/otto/auth.json or ~/.local/state/otto/auth.json
|
|
59
|
+
// - macOS: ~/Library/Application Support/otto/auth.json
|
|
60
|
+
// - Windows: %APPDATA%\otto\auth.json
|
|
61
|
+
export function getSecureAuthPath(): string {
|
|
62
|
+
return joinPath(getSecureBaseDir(), 'auth.json');
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
// Global content under config dir
|
package/src/core/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MCPServerManager } from './server-manager.ts';
|
|
2
|
-
import type { MCPConfig, MCPServerConfig } from './types.ts';
|
|
2
|
+
import type { MCPConfig, MCPServerConfig, MCPScope } from './types.ts';
|
|
3
3
|
import { promises as fs } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
|
|
@@ -11,11 +11,15 @@ export function getMCPManager(): MCPServerManager | null {
|
|
|
11
11
|
|
|
12
12
|
export async function initializeMCP(
|
|
13
13
|
config: MCPConfig,
|
|
14
|
+
projectRoot?: string,
|
|
14
15
|
): Promise<MCPServerManager> {
|
|
15
16
|
if (globalMCPManager) {
|
|
16
17
|
await globalMCPManager.stopAll();
|
|
17
18
|
}
|
|
18
19
|
globalMCPManager = new MCPServerManager();
|
|
20
|
+
if (projectRoot) {
|
|
21
|
+
globalMCPManager.setProjectRoot(projectRoot);
|
|
22
|
+
}
|
|
19
23
|
await globalMCPManager.startServers(config.servers);
|
|
20
24
|
return globalMCPManager;
|
|
21
25
|
}
|
|
@@ -41,7 +45,7 @@ export async function loadMCPConfig(
|
|
|
41
45
|
const globalServers = await readMCPServersFromFile(globalPath);
|
|
42
46
|
for (const s of globalServers) {
|
|
43
47
|
seen.add(s.name);
|
|
44
|
-
servers.push(s);
|
|
48
|
+
servers.push({ ...s, scope: 'global' });
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -50,9 +54,9 @@ export async function loadMCPConfig(
|
|
|
50
54
|
for (const s of projectServers) {
|
|
51
55
|
if (seen.has(s.name)) {
|
|
52
56
|
const idx = servers.findIndex((existing) => existing.name === s.name);
|
|
53
|
-
if (idx >= 0) servers[idx] = s;
|
|
57
|
+
if (idx >= 0) servers[idx] = { ...s, scope: 'project' };
|
|
54
58
|
} else {
|
|
55
|
-
servers.push(s);
|
|
59
|
+
servers.push({ ...s, scope: 'project' });
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -81,11 +85,30 @@ async function readMCPServersFromFile(
|
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
function resolveConfigPath(
|
|
89
|
+
projectRoot: string,
|
|
90
|
+
globalConfigDir: string | undefined,
|
|
91
|
+
scope: MCPScope,
|
|
92
|
+
): string {
|
|
93
|
+
if (scope === 'global' && globalConfigDir) {
|
|
94
|
+
return join(globalConfigDir, 'config.json');
|
|
95
|
+
}
|
|
96
|
+
return join(projectRoot, '.otto', 'config.json');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function ensureConfigDir(configPath: string): Promise<void> {
|
|
100
|
+
const dir = configPath.replace(/[/\\][^/\\]+$/, '');
|
|
101
|
+
await fs.mkdir(dir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
export async function addMCPServerToConfig(
|
|
85
105
|
projectRoot: string,
|
|
86
106
|
server: MCPServerConfig,
|
|
107
|
+
globalConfigDir?: string,
|
|
87
108
|
): Promise<void> {
|
|
88
|
-
const
|
|
109
|
+
const scope: MCPScope = server.scope ?? 'global';
|
|
110
|
+
const configPath = resolveConfigPath(projectRoot, globalConfigDir, scope);
|
|
111
|
+
|
|
89
112
|
let json: Record<string, unknown> = {};
|
|
90
113
|
try {
|
|
91
114
|
const text = await fs.readFile(configPath, 'utf-8');
|
|
@@ -98,37 +121,48 @@ export async function addMCPServerToConfig(
|
|
|
98
121
|
|
|
99
122
|
const servers = mcp.servers as MCPServerConfig[];
|
|
100
123
|
const idx = servers.findIndex((s) => s.name === server.name);
|
|
124
|
+
|
|
125
|
+
const { scope: _scope, ...serverWithoutScope } = server;
|
|
101
126
|
if (idx >= 0) {
|
|
102
|
-
servers[idx] =
|
|
127
|
+
servers[idx] = serverWithoutScope;
|
|
103
128
|
} else {
|
|
104
|
-
servers.push(
|
|
129
|
+
servers.push(serverWithoutScope);
|
|
105
130
|
}
|
|
106
131
|
|
|
107
|
-
await
|
|
132
|
+
await ensureConfigDir(configPath);
|
|
108
133
|
await fs.writeFile(configPath, JSON.stringify(json, null, '\t'), 'utf-8');
|
|
109
134
|
}
|
|
110
135
|
|
|
111
136
|
export async function removeMCPServerFromConfig(
|
|
112
137
|
projectRoot: string,
|
|
113
138
|
name: string,
|
|
139
|
+
globalConfigDir?: string,
|
|
114
140
|
): Promise<boolean> {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
const paths = [
|
|
142
|
+
...(globalConfigDir ? [join(globalConfigDir, 'config.json')] : []),
|
|
143
|
+
join(projectRoot, '.otto', 'config.json'),
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
for (const configPath of paths) {
|
|
147
|
+
let json: Record<string, unknown> = {};
|
|
148
|
+
try {
|
|
149
|
+
const text = await fs.readFile(configPath, 'utf-8');
|
|
150
|
+
json = JSON.parse(text);
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
123
154
|
|
|
124
|
-
|
|
125
|
-
|
|
155
|
+
const mcp = json.mcp as Record<string, unknown> | undefined;
|
|
156
|
+
if (!mcp || !Array.isArray(mcp.servers)) continue;
|
|
126
157
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
158
|
+
const servers = mcp.servers as MCPServerConfig[];
|
|
159
|
+
const idx = servers.findIndex((s) => s.name === name);
|
|
160
|
+
if (idx < 0) continue;
|
|
130
161
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
162
|
+
servers.splice(idx, 1);
|
|
163
|
+
await fs.writeFile(configPath, JSON.stringify(json, null, '\t'), 'utf-8');
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return false;
|
|
134
168
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { getSecureOAuthDir } from '../../../../config/src/paths.ts';
|
|
3
4
|
|
|
4
5
|
export interface StoredOAuthData {
|
|
5
6
|
tokens?: {
|
|
@@ -22,14 +23,7 @@ export class OAuthCredentialStore {
|
|
|
22
23
|
private storePath: string;
|
|
23
24
|
|
|
24
25
|
constructor(storePath?: string) {
|
|
25
|
-
this.storePath =
|
|
26
|
-
storePath ??
|
|
27
|
-
join(
|
|
28
|
-
process.env.HOME ?? process.env.USERPROFILE ?? '',
|
|
29
|
-
'.config',
|
|
30
|
-
'otto',
|
|
31
|
-
'oauth',
|
|
32
|
-
);
|
|
26
|
+
this.storePath = storePath ?? getSecureOAuthDir();
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
private filePath(serverName: string): string {
|
|
@@ -2,6 +2,7 @@ import { MCPClientWrapper, type MCPToolInfo } from './client.ts';
|
|
|
2
2
|
import type { MCPServerConfig, MCPServerStatus } from './types.ts';
|
|
3
3
|
import { OAuthCredentialStore } from './oauth/store.ts';
|
|
4
4
|
import { OttoOAuthProvider } from './oauth/provider.ts';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
|
|
6
7
|
type IndexedTool = {
|
|
7
8
|
server: string;
|
|
@@ -14,17 +15,36 @@ export class MCPServerManager {
|
|
|
14
15
|
private authProviders = new Map<string, OttoOAuthProvider>();
|
|
15
16
|
private pendingAuth = new Map<string, string>();
|
|
16
17
|
private oauthStore = new OAuthCredentialStore();
|
|
18
|
+
private serverScopes = new Map<string, 'global' | 'project'>();
|
|
17
19
|
private _started = false;
|
|
20
|
+
private projectRoot: string | null = null;
|
|
18
21
|
|
|
19
22
|
get started(): boolean {
|
|
20
23
|
return this._started;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
setProjectRoot(projectRoot: string): void {
|
|
27
|
+
this.projectRoot = projectRoot;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private oauthKey(serverName: string): string {
|
|
31
|
+
const scope = this.serverScopes.get(serverName);
|
|
32
|
+
if (scope === 'project' && this.projectRoot) {
|
|
33
|
+
const hash = createHash('sha256')
|
|
34
|
+
.update(this.projectRoot)
|
|
35
|
+
.digest('hex')
|
|
36
|
+
.slice(0, 8);
|
|
37
|
+
return `${serverName}_proj_${hash}`;
|
|
38
|
+
}
|
|
39
|
+
return serverName;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
async startServers(configs: MCPServerConfig[]): Promise<void> {
|
|
24
43
|
await this.stopAll();
|
|
25
44
|
|
|
26
45
|
for (const config of configs) {
|
|
27
46
|
if (config.disabled) continue;
|
|
47
|
+
this.serverScopes.set(config.name, config.scope ?? 'global');
|
|
28
48
|
await this.startSingleServer(config);
|
|
29
49
|
}
|
|
30
50
|
this._started = true;
|
|
@@ -38,7 +58,8 @@ export class MCPServerManager {
|
|
|
38
58
|
const hasStaticAuth =
|
|
39
59
|
config.headers?.Authorization || config.headers?.authorization;
|
|
40
60
|
if (!hasStaticAuth) {
|
|
41
|
-
const
|
|
61
|
+
const key = this.oauthKey(config.name);
|
|
62
|
+
const provider = new OttoOAuthProvider(key, this.oauthStore, {
|
|
42
63
|
clientId: config.oauth?.clientId,
|
|
43
64
|
callbackPort: config.oauth?.callbackPort,
|
|
44
65
|
scopes: config.oauth?.scopes,
|
|
@@ -104,6 +125,7 @@ export class MCPServerManager {
|
|
|
104
125
|
this.toolsMap.clear();
|
|
105
126
|
this.authProviders.clear();
|
|
106
127
|
this.pendingAuth.clear();
|
|
128
|
+
this.serverScopes.clear();
|
|
107
129
|
this._started = false;
|
|
108
130
|
}
|
|
109
131
|
|
|
@@ -137,8 +159,9 @@ export class MCPServerManager {
|
|
|
137
159
|
.filter(([, v]) => v.server === name)
|
|
138
160
|
.map(([k]) => k);
|
|
139
161
|
const config = client.serverConfig;
|
|
162
|
+
const key = this.oauthKey(name);
|
|
140
163
|
const _authenticated = this.oauthStore
|
|
141
|
-
.isAuthenticated(
|
|
164
|
+
.isAuthenticated(key)
|
|
142
165
|
.catch(() => false);
|
|
143
166
|
|
|
144
167
|
statuses.push({
|
|
@@ -161,8 +184,9 @@ export class MCPServerManager {
|
|
|
161
184
|
.filter(([, v]) => v.server === name)
|
|
162
185
|
.map(([k]) => k);
|
|
163
186
|
const config = client.serverConfig;
|
|
187
|
+
const key = this.oauthKey(name);
|
|
164
188
|
const authenticated = await this.oauthStore
|
|
165
|
-
.isAuthenticated(
|
|
189
|
+
.isAuthenticated(key)
|
|
166
190
|
.catch(() => false);
|
|
167
191
|
|
|
168
192
|
statuses.push({
|
|
@@ -194,7 +218,9 @@ export class MCPServerManager {
|
|
|
194
218
|
const transport = config.transport ?? 'stdio';
|
|
195
219
|
if (transport === 'stdio') return null;
|
|
196
220
|
|
|
197
|
-
|
|
221
|
+
this.serverScopes.set(config.name, config.scope ?? 'global');
|
|
222
|
+
const key = this.oauthKey(config.name);
|
|
223
|
+
const provider = new OttoOAuthProvider(key, this.oauthStore, {
|
|
198
224
|
clientId: config.oauth?.clientId,
|
|
199
225
|
callbackPort: config.oauth?.callbackPort,
|
|
200
226
|
scopes: config.oauth?.scopes,
|
|
@@ -272,7 +298,8 @@ export class MCPServerManager {
|
|
|
272
298
|
async getAuthStatus(
|
|
273
299
|
name: string,
|
|
274
300
|
): Promise<{ authenticated: boolean; expiresAt?: number }> {
|
|
275
|
-
const
|
|
301
|
+
const key = this.oauthKey(name);
|
|
302
|
+
const tokens = await this.oauthStore.loadTokens(key);
|
|
276
303
|
if (!tokens?.access_token) return { authenticated: false };
|
|
277
304
|
return {
|
|
278
305
|
authenticated: true,
|
|
@@ -282,6 +309,7 @@ export class MCPServerManager {
|
|
|
282
309
|
|
|
283
310
|
async restartServer(config: MCPServerConfig): Promise<void> {
|
|
284
311
|
await this.stopServer(config.name);
|
|
312
|
+
this.serverScopes.set(config.name, config.scope ?? 'global');
|
|
285
313
|
await this.startSingleServer(config);
|
|
286
314
|
}
|
|
287
315
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type MCPTransport = 'stdio' | 'http' | 'sse';
|
|
2
|
+
export type MCPScope = 'global' | 'project';
|
|
2
3
|
|
|
3
4
|
export interface MCPOAuthConfig {
|
|
4
5
|
clientId?: string;
|
|
@@ -21,6 +22,8 @@ export interface MCPServerConfig {
|
|
|
21
22
|
oauth?: MCPOAuthConfig;
|
|
22
23
|
|
|
23
24
|
disabled?: boolean;
|
|
25
|
+
|
|
26
|
+
scope?: MCPScope;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface MCPConfig {
|
package/src/index.ts
CHANGED
|
@@ -171,6 +171,8 @@ export {
|
|
|
171
171
|
getGlobalToolsDir,
|
|
172
172
|
getGlobalCommandsDir,
|
|
173
173
|
getSecureAuthPath,
|
|
174
|
+
getSecureBaseDir,
|
|
175
|
+
getSecureOAuthDir,
|
|
174
176
|
getHomeDir,
|
|
175
177
|
} from './config/src/paths.ts';
|
|
176
178
|
export {
|
|
@@ -358,6 +360,7 @@ export type {
|
|
|
358
360
|
MCPToolInfo,
|
|
359
361
|
MCPTransport,
|
|
360
362
|
MCPOAuthConfig,
|
|
363
|
+
MCPScope,
|
|
361
364
|
StoredOAuthData,
|
|
362
365
|
OttoOAuthProviderOptions,
|
|
363
366
|
CallbackResult,
|