Standalone MCP Manager
Use MCPManager from @dexto/core/mcp to connect to MCP servers and call their tools, prompts, and resources directly. You do not need a Dexto agent, an LLM, or a Dexto Cloud account.
Install and connect
pnpm add @dexto/core
This example connects to an HTTP MCP server you already run. Replace the URL and tool name with your server's values.
import { AgentEventBus } from '@dexto/core/events';
import { createLogger } from '@dexto/core/logger';
import { MCPManager, McpServerConfigSchema } from '@dexto/core/mcp';
const logger = createLogger({
agentId: 'standalone-mcp',
config: {
level: 'warn',
transports: [{ type: 'console', colorize: false }],
},
});
const manager = new MCPManager(logger, new AgentEventBus());
const config = McpServerConfigSchema.parse({
type: 'http',
url: 'http://localhost:3001/mcp',
});
try {
await manager.connectServer('local', config);
console.log(Object.keys(await manager.getAllTools()));
// Validate arguments separately before making a direct tool call.
const args = manager.validateToolInput('lookup', { query: 'example' });
const result = await manager.executeTool('lookup', args);
console.log(result);
} finally {
await manager.disconnectAll();
}
An explicit event bus keeps manager events separate from other Dexto runtimes in the same process. Omitting it uses the shared default event bus.
For a local stdio server, parse a config with type: 'stdio', command, args, and an optional env object. The manager starts that command as a child process. Give it only the environment variables it needs. HTTP and SSE configurations use url and optional headers; prefer HTTP for new remote servers.
Discovery and calls
getAllTools()returns cached tools and their callable names. Duplicate tool names are qualified assanitized-server--tool; use the names returned by discovery. Removing a conflicting server can restore a simple name.validateToolInput(name, args)validates against the cached tool schema.executeTool(name, args)routes the call; it does not perform this validation for you.listAllPrompts()andgetAllPromptMetadata()discover cached prompts;getPrompt(name, args)asks their server to render one. Duplicate prompt names currently use the last cached provider.listAllResources()returns resources with qualifiedkeyvalues. Pass that key toreadResource(key).refresh()explicitly refreshes discovery. Server notifications also update caches.
Lifecycle and failures
Same-name registerClient() replacement remains supported. A displaced different client stays usable for already captured calls and remains manager-owned until disconnectAll(), which disconnects current and displaced identities once each per cleanup pass. Individual removeClient(name) and restart operations still target the currently registered client; this does not change cache ordering or consistency during overlapping connections.
Always call disconnectAll() in a finally block. Use removeClient(name) to disconnect and forget one connection, or restartServer(name) to reconnect using its saved configuration.
Before teardown, stop initiating new connections and restarts. disconnectAll() drains operations already pending, including their handshake and discovery, then disconnects registered clients and clears caches. Original connection/restart results are preserved, desired registrations remain, and the manager can be reused. Draining waits for existing protocol requests and caller-owned authentication; it does not cancel startup or add a deadline. Connections/restarts started after teardown begins and independent refresh/notification work are outside this guarantee.
A failed connection or restart attempts to disconnect its rejected candidate. A cleanup error does not replace the connection error. A failed restart retains its saved configuration so you can retry it; it does not restore the old connection. Other connected servers remain usable.
getFailedConnections() reports recorded connection errors. getFailedConnectionError(name) and getFailedConnectionErrorCode(name) expose their details. These describe failed connection attempts; they are not a continuous remote-health check.
initializeFromConfig(configs) connects enabled servers concurrently. Each server's connectionMode controls failure handling: strict failures reject initialization, while lenient failures are recorded without rejecting it. The default is lenient. Successful peers remain connected even if initialization rejects, so cleanup still belongs in finally.
Server names must remain unique after sanitization: characters outside letters, numbers, underscores, and hyphens become underscores. For example, my@server and my_server collide.
Desired configuration and safe status
Hosts that need to display configured entries can opt into desired configuration ownership:
manager.configureServer('local', config);
console.log(manager.getConfiguredServerStatuses());
// [{ name: 'local', configuredTransport: 'http', status: 'configured' }]
try {
await manager.connectConfiguredServer('local');
} finally {
await manager.disconnectAll();
manager.forgetServerConfiguration('local');
}
configureServer(name, config) copies validated configuration without connecting. The name __proto__ is reserved because legacy failure storage cannot record it safely; this restriction applies only to the new registration API. Names such as constructor and toString remain supported. Disabled entries appear as disabled; attempting to connect them rejects without starting a process. connectConfiguredServer(name) requires a registered, enabled configuration and rejects names with an active attempt or registered client. Configure and forget also reject active names; disconnect before replacing or forgetting configuration.
getConfiguredServerStatuses() includes only names registered with configureServer. It returns fresh readonly metadata: name, configuredTransport, status, and an allowlisted errorCode only for failed. Statuses are configured, disabled, connecting, connected, or failed. While any connection or restart attempt for the name is in flight, connecting takes precedence. connected means a client is registered; it does not promise remote liveness. Failure describes the last recorded attempt for that name, not verification of its desired configuration.
configuredTransport always describes desired configuration. A legacy connectServer(name, otherConfig) call can connect another transport for the same name without applying desired configuration. The snapshot reports registration state while keeping desired transport metadata distinct.
Desired registration survives removeClient() and reusable disconnectAll() until explicitly forgotten. getServerConfig() continues to describe legacy successful/restart configuration; raw error getters and existing agent status APIs keep their original behavior. Desired configuration and legacy restart configuration are independently owned. No configuration, command, URL, environment, headers, process metadata, client object or raw error message is included in the new snapshot.
Authorization and current limits
Direct MCP management does not install the agent's tool permission policy or display a CLI approval prompt. Your application must authorize calls and supply the server's required credentials. Discovery and argument validation do not grant permission to execute a tool. An optional approval manager handles server elicitation; it is not a substitute for authorization around direct tool calls.
setAuthProviderFactory(factory) supplies an OAuth provider for servers that need one. Static credentials can be supplied through configured headers or a stdio server's environment. Keep credentials out of discovery output and application logs.
callToolDirect accepts a discovered MCP descriptor identity, object arguments and an optional caller-owned AbortSignal. It preserves full MCP results, including isError, structured content and metadata. Literal connection/tool names are independent of aliases. The caller owns authorization; this method does not install agent execution policy.
Direct calls use the saved restart configuration timeout for their connection name, independently of desired configuration. Externally registered clients without saved configuration retain the SDK default. A manual replacement under a name with saved legacy configuration retains that name-owned timeout. Cancellation leaves the client connected and does not guarantee upstream side-effect rollback. Absent or initializing/restarting connections reject. Existing executeTool error conversion and agent context remain unchanged. Disconnect is best-effort, and connection status is not a remote-health probe.
See the MCPManager API reference for method signatures.
Operational diagnostics
Core MCP logs describe operations using selected server/tool/prompt names and fixed Core error classifications. They omit raw stdio configuration, remote URLs, resource URIs, capability payloads, elicitation content and provider error text. Keep credentials out of chosen names. Server stderr and application or SDK logging outside these Core call sites remain separately owned.
DextoMcpClient.getServerInfo() returns raw command, arguments and environment alongside local process information. getServerConfig() and failed-connection inspection also return raw caller-owned data, and thrown errors retain their existing details. Do not serialize these APIs as public status output or ordinary logs. Use getConnectionStatus() for local connection state; it is not a remote-health probe.
MCP SDK v1 compatibility
Dexto uses MCP SDK 1.32.1 within the monolithic v1 package. This keeps the existing client and manager APIs.
HTTP and SSE transports now restrict redirects to the original origin by default. Method-preserving same-origin redirects remain supported, as does the SDK's same-host HTTP-to-HTTPS upgrade exception with default ports. For services that redirect to another host or port, set url to the final MCP endpoint. The same redirect default applies to the transport's OAuth HTTP requests; OAuth services must publish direct metadata and token endpoint URLs. Dexto does not opt into unrestricted redirect following.
The SDK now limits pending stdio protocol input buffering to 10 MiB. Oversized pending input is rejected; this is a transport input bound, not an agent task limit. Dexto currently exposes no override for this bound.
OAuth token and client records can include optional issuer metadata. Dexto's CLI JSON store retains complete records. Custom OAuth stores must preserve these fields; upstream still accepts legacy issuerless refresh records with a warning.
See the official 1.32.0 redirect notes, 1.31.0 OAuth notes and 1.30.0 stdio notes.