Skip to content
Merged
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
10 changes: 4 additions & 6 deletions ts/packages/agentServer/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import {
} from "agent-dispatcher/helpers/data";
import {
getDefaultAppAgentProviders,
getDefaultAppAgentSource,
getMcpAppAgentSource,
getDefaultAppAgentSources,
getIndexingServiceRegistry,
getDefaultConstructionProvider,
} from "default-agent-provider";
Expand Down Expand Up @@ -340,10 +339,9 @@ async function main() {
instanceDir,
configName,
),
appAgentSources: [
getDefaultAppAgentSource(instanceDir, { configName }),
getMcpAppAgentSource(instanceDir),
],
appAgentSources: getDefaultAppAgentSources(instanceDir, {
configName,
}),
persistSession: true,
storageProvider: getFsStorageProvider(),
metrics: true,
Expand Down
12 changes: 4 additions & 8 deletions ts/packages/api/src/webDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import { createDispatcherRpcServer } from "@typeagent/dispatcher-rpc/dispatcher/
import { createChannelAdapter } from "@typeagent/agent-rpc/channel";
import {
getDefaultAppAgentProviders,
getDefaultAppAgentSource,
getMcpAppAgentSource,
getDefaultAppAgentSources,
getDefaultConstructionProvider,
getIndexingServiceRegistry,
} from "default-agent-provider";
Expand Down Expand Up @@ -54,12 +53,9 @@ export async function createWebDispatcher(
const clientIO = createClientIORpcClient(clientIOChannel.channel);
const dispatcher = await createDispatcher("api", {
appAgentProviders: getDefaultAppAgentProviders(instanceDir),
appAgentSources: [
getDefaultAppAgentSource(instanceDir, {
excludePathSources: true,
}),
getMcpAppAgentSource(instanceDir),
],
appAgentSources: getDefaultAppAgentSources(instanceDir, {
excludePathSources: true,
}),
persistSession: true,
persistDir: instanceDir,
storageProvider: getFsStorageProvider(),
Expand Down
48 changes: 47 additions & 1 deletion ts/packages/defaultAgentProvider/src/defaultAgentProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,20 @@ import {
InstallPreview,
InstallPreviewMatch,
InstallResult,
McpInstallCandidate,
UpdateResult,
deriveMatchKind,
} from "./installSources/config.js";
import { cleanupOwnedMcpPaths } from "./installSources/mcpRegistryMaterializer.js";
import {
createPackageAppAgentProvider,
AgentSourceGroup,
AvailableAgentInfo,
InstalledAgentInfo,
InstalledAgentSourceApi,
} from "./installSources/packageAgent.js";
import type { McpServerSourceApi } from "./mcp/mcpAppAgentSource.js";
import type { NormalizedMcpServerConfig } from "./mcp/mcpServerConfig.js";

import fs from "node:fs";
import path from "node:path";
Expand Down Expand Up @@ -375,6 +379,8 @@ export function createDefaultInstalledAgentSource(
sourceFactory?: InstallSourceFactory,
/** @internal Test-only store writer for exercising commit failures. */
storeWriter: typeof writeAgentsJson = writeAgentsJson,
/** MCP runtime API shared with this source's per-session @package agent. */
mcpSource?: McpServerSourceApi,
): InstalledAgentSourceForTest {
const instanceConfigs = getInstanceConfigProvider(instanceDir);
const installDir = getInstallDir(instanceConfigs);
Expand Down Expand Up @@ -1265,9 +1271,18 @@ export function createDefaultInstalledAgentSource(
registry,
recordsUsingSource: (sourceName: string) => {
const agents = readAgentsJson(instanceDir)?.agents ?? {};
return Object.values(agents)
const agentNames = Object.values(agents)
.filter((record) => record.source === sourceName)
.map((record) => record.name);
const mcpNames =
mcpSource
?.listServers()
.filter(
(config) =>
config.provenance.source === sourceName,
)
.map((config) => config.name) ?? [];
return [...agentNames, ...mcpNames];
},
});
},
Expand Down Expand Up @@ -1442,6 +1457,36 @@ export function createDefaultInstalledAgentSource(
matches: result.matches.map(toMatch),
};
},
async resolveMcp(
ref: string,
sourceName?: string,
onStatus?: SourceStatus,
): Promise<McpInstallCandidate[]> {
return registry.resolveMcp(ref, sourceName, undefined, onStatus);
},
async materializeMcp(
candidate: McpInstallCandidate,
abortSignal?: AbortSignal,
): Promise<NormalizedMcpServerConfig> {
return candidate.materialize === undefined
? candidate.config
: candidate.materialize(abortSignal);
},
cleanupMcp(config: NormalizedMcpServerConfig): void {
const referencedPaths = new Set(
mcpSource
?.listServers()
.filter((other) => other.id !== config.id)
.flatMap((other) => other.provenance.ownedPaths ?? []) ??
[],
);
cleanupOwnedMcpPaths(
resolvedInstallDir,
config.provenance.ownedPaths?.filter(
(ownedPath) => !referencedPaths.has(ownedPath),
),
);
},
async refresh(sourceName?: string): Promise<void> {
// Refresh cache-backed source metadata; a fetch failure propagates
// so the `--refresh` command fails rather than acting on stale data.
Expand All @@ -1460,6 +1505,7 @@ export function createDefaultInstalledAgentSource(
const packageProvider = createPackageAppAgentProvider({
appAgentProviderSetController: controller,
source,
...(mcpSource === undefined ? {} : { mcpSource }),
});
// Torn down before the initial set resolved: a connection disposed
// while still parked on an in-flight barrier must NOT join the
Expand Down
63 changes: 63 additions & 0 deletions ts/packages/defaultAgentProvider/src/defaultAgentRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type { AppAgentSource } from "agent-dispatcher";
import {
createDefaultInstalledAgentSource,
DefaultAppAgentSourceOptions,
} from "./defaultAgentProviders.js";
import { createMcpAppAgentSourceForInstance } from "./mcpDefaultAgentProvider.js";
import { getInstanceConfigProvider } from "./utils/config.js";
import type { InstalledAgentSourceApi } from "./installSources/packageAgent.js";
import type { McpServerSourceApi } from "./mcp/mcpAppAgentSource.js";
import type { McpHostServices } from "./mcp/mcpServerProvider.js";
import { SessionMcpCredentialStore } from "./mcp/mcpCredentialStore.js";
import { defaultMcpPolicy } from "./mcp/mcpPolicy.js";
import { JsonlMcpAuditSink } from "./mcp/mcpAudit.js";

export interface DefaultAgentRuntime {
readonly appAgentSources: [AppAgentSource, AppAgentSource];
readonly installedAgentSourceApi: InstalledAgentSourceApi;
readonly mcpServerSourceApi: McpServerSourceApi;
}

export function createDefaultAgentRuntime(
instanceDir: string,
options?: DefaultAppAgentSourceOptions,
mcpServices?: Partial<McpHostServices>,
): DefaultAgentRuntime {
const services: McpHostServices = {
credentialStore:
mcpServices?.credentialStore ?? new SessionMcpCredentialStore(),
policy: mcpServices?.policy ?? defaultMcpPolicy,
audit: mcpServices?.audit ?? new JsonlMcpAuditSink(instanceDir),
...(mcpServices?.oauthInteraction === undefined
? {}
: { oauthInteraction: mcpServices.oauthInteraction }),
};
const mcp = createMcpAppAgentSourceForInstance(
getInstanceConfigProvider(instanceDir),
services,
);
const installed = createDefaultInstalledAgentSource(
instanceDir,
options,
undefined,
undefined,
mcp.testApi,
);
const { testApi: installedAgentSourceApi, ...installedSource } = installed;
const { testApi: mcpServerSourceApi, ...mcpSource } = mcp;
return {
appAgentSources: [installedSource, mcpSource],
installedAgentSourceApi,
mcpServerSourceApi,
};
}

export function getDefaultAppAgentSources(
instanceDir: string,
options?: DefaultAppAgentSourceOptions,
): [AppAgentSource, AppAgentSource] {
return createDefaultAgentRuntime(instanceDir, options).appAgentSources;
}
22 changes: 22 additions & 0 deletions ts/packages/defaultAgentProvider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,29 @@ export {
getIndexingServiceRegistry,
} from "./defaultAgentProviders.js";
export { getMcpAppAgentSource } from "./mcpDefaultAgentProvider.js";
export {
createDefaultAgentRuntime,
getDefaultAppAgentSources,
type DefaultAgentRuntime,
} from "./defaultAgentRuntime.js";
export { getDefaultConstructionProvider } from "./defaultConstructionProvider.js";
export {
SessionMcpCredentialStore,
type McpCredentialStore,
} from "./mcp/mcpCredentialStore.js";
export {
defaultMcpPolicy,
enforceMcpPolicy,
type McpPolicy,
} from "./mcp/mcpPolicy.js";
export {
JsonlMcpAuditSink,
sanitizeMcpAuditEvent,
type McpAuditEvent,
type McpAuditSink,
} from "./mcp/mcpAudit.js";
export type { McpOAuthInteraction } from "./mcp/mcpOAuth.js";
export type { McpHostServices } from "./mcp/mcpServerProvider.js";
export {
createOnboardingOnlyDispatcher,
type OnboardingDispatcherHandle,
Expand Down
70 changes: 70 additions & 0 deletions ts/packages/defaultAgentProvider/src/installSources/addSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
FeedSourceConfig,
McpConfigSourceConfig,
PathSourceConfig,
RegistrySourceConfig,
} from "./config.js";
import { DefaultInstallSourceRegistry } from "./registry.js";
import { expandHome } from "./paths.js";
Expand All @@ -35,11 +36,30 @@ function validateFeedRegistry(url: string): void {
} catch {
throw new Error(`'${url}' is not a well-formed URL`);
}

if (parsed.protocol !== "https:") {
throw new Error(`feed registry URL must be https: '${url}'`);
}
}

function validateRegistryUrl(url: string): string {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
throw new Error(`'${url}' is not a well-formed URL`);
}
if (parsed.protocol !== "https:") {
throw new Error(`MCP Registry URL must be https: '${url}'`);
}
parsed.search = "";
parsed.hash = "";
if (!parsed.pathname.endsWith("/")) {
parsed.pathname += "/";
}
return parsed.toString();
}

function validateCatalogFile(catalog: string): void {
try {
JSON.parse(fs.readFileSync(catalog, "utf8"));
Expand Down Expand Up @@ -208,6 +228,7 @@ class McpConfigAddCommandHandler implements CommandHandler {
"--file <path> is required for an mcp-config source",
);
}

const normalizedFile = normalizeAbsolutePath(file);
try {
JSON.parse(fs.readFileSync(normalizedFile, "utf8"));
Expand All @@ -218,6 +239,7 @@ class McpConfigAddCommandHandler implements CommandHandler {
`MCP config file '${normalizedFile}' is not accessible: ${err.message}`,
);
}

throw new Error(
`MCP config '${normalizedFile}' is not valid JSON: ${err.message}`,
);
Expand All @@ -232,6 +254,53 @@ class McpConfigAddCommandHandler implements CommandHandler {
}
}

class RegistryAddCommandHandler implements CommandHandler {
public readonly description = "Add an MCP Registry v0.1 install source";
public readonly parameters = {
args: {
name: { description: "Unique source name", type: "string" },
},
flags: {
url: {
description: "MCP Registry base URL (https)",
char: "u",
type: "string",
},
"cache-ttl": {
description: "Metadata cache TTL in seconds",
type: "number",
optional: true,
},
},
} as const;
constructor(private readonly registry: DefaultInstallSourceRegistry) {}
public async run(
context: ActionContext<unknown>,
params: ParsedCommandParams<typeof this.parameters>,
) {
const url = params.flags.url;
if (url === undefined) {
throw new Error(
"--url <https-url> is required for a registry source",
);
}
const ttlSeconds = params.flags["cache-ttl"];
if (ttlSeconds !== undefined && ttlSeconds <= 0) {
throw new Error("--cache-ttl must be greater than zero");
}
const config: RegistrySourceConfig = {
kind: "registry",
name: params.args.name,
baseUrl: validateRegistryUrl(url),
...(ttlSeconds === undefined
? {}
: { cacheTtlMs: ttlSeconds * 1000 }),
};
this.registry.add(config);
displayResult(`Added registry source '${params.args.name}'.`, context);
}
}

/**
* Build the host's `@package source add` subcommand table
* merges this into the `@package source` table via
Expand All @@ -247,6 +316,7 @@ export function getAddSourceCommandHandlers(
catalog: new CatalogAddCommandHandler(registry),
path: new PathAddCommandHandler(registry),
"mcp-config": new McpConfigAddCommandHandler(registry),
registry: new RegistryAddCommandHandler(registry),
},
};
}
Loading
Loading