AI SDK MCP Client: Connect JavaScript to an MCP Server
Introduction
To connect a JavaScript application to a Model Context Protocol (MCP) server with the Vercel AI SDK, you create an MCP client with createMCPClient from the @ai-sdk/mcp package (experimental_createMCPClient in older versions), pass the tools it returns to generateText or streamText, and close the client when you're done.
The model then decides which of the server's tools to call, and the AI SDK runs them and feeds the results back to the model.
MCP provides a standardized way for artificial intelligence (AI) models to interact with external tools and systems. Check my introduction to MCP if you're new to the protocol. Vercel AI SDK is a TypeScript toolkit with a growing list of supported model providers, which makes it an excellent alternative to the LangChain.js library.
In this post, I'll show you how to connect your JavaScript applications to an MCP server using the AI SDK.
I'll describe how to set up the MCP client over stdio and Streamable HTTP, configure a model provider, and prompt the model using the generateText function.
I tested every step in October 2026 with AI SDK 7.0.140, @ai-sdk/mcp 2.0.74, the Kubernetes MCP Server v0.0.67, and a small local model (Qwen3 4B) running in Ollama, so you can follow along without an API key.
Which import do you need: experimental_createMCPClient or createMCPClient?
Vercel added an experimental MCP client in AI SDK 4.2 (March 2025), and I first wrote this post a few weeks later, in April 2025.
The client moved to its own package, @ai-sdk/mcp, in October 2025, and AI SDK 6 (December 2025) made it stable and dropped the experimental_ prefix, so the import in older tutorials depends on the AI SDK version they target:
| AI SDK version | MCP client import |
|---|---|
| 4.2 to 5.0.78 | experimental_createMCPClient from ai |
| 5.0.79 to 5.x | experimental_createMCPClient from @ai-sdk/mcp (0.x) |
| 6 and 7 | createMCPClient from @ai-sdk/mcp (1.x and 2.x) |
As of October 2026, @ai-sdk/mcp still exports the experimental_createMCPClient name as an alias, but new code should use createMCPClient.
AI SDK 7 (June 2026) made every package ESM-only and renamed some of the functions this post uses.
If you're upgrading code from AI SDK 6, these are the changes from its migration guide that affect this post:
stepCountIsis nowisStepCount(the old name still works as an alias).- The client option
nameis deprecated in favor ofclientName, which late AI SDK 6 releases already accept (namestill works). - The HTTP transport rejects redirects by default; set
redirect: 'follow'if your server relies on them. - All packages are ESM-only and need Node.js 22 or later.
Setting up the project
The AI SDK 7 packages are ESM-only and need Node.js 22 or later (check with node --version).
Create a project and switch it to ES modules:
npm init -y
npm pkg set type=moduleInstall the AI SDK, its MCP client, the client package of the official MCP TypeScript SDK (for the stdio transport), and the Ollama provider:
npm install ai @ai-sdk/mcp @modelcontextprotocol/client ollama-ai-provider-v2The examples use a local model through Ollama. Install Ollama and pull a small model that supports tool calling (about 2.5 GB):
ollama pull qwen3:4b-instructThe Kubernetes MCP Server needs a cluster to talk to.
It uses your default kubeconfig file, so any cluster works; I used a local kind cluster, which kind create cluster creates and adds to that file.
Creating the AI SDK MCP clients
To interact with an MCP server, we first need to create an MCP client.
AI SDK provides a function called createMCPClient in the @ai-sdk/mcp package that allows us to establish this connection.
The MCP specification defines two standard transports: stdio and Streamable HTTP. Let's learn how to create a client for each of them.
Setting up an STDIO transport client
The STDIO transport method allows us to spawn a local process and communicate with it through standard input and output streams. This is useful for running local MCP servers or processes that expose the local system's capabilities.
The following code snippet demonstrates how to set up an STDIO transport client for the Kubernetes MCP server:
import {createMCPClient} from '@ai-sdk/mcp';
import {StdioClientTransport} from '@modelcontextprotocol/client/stdio';
const initStdioClient = async () => {
const transport = new StdioClientTransport({
command: 'npx',
args: ['-y', 'kubernetes-mcp-server@latest']
});
return createMCPClient({clientName: 'blog.marcnuri.com', transport});
};In this code, we're creating an MCP client instance that runs the Kubernetes MCP server using the npx command.
These are the key steps to set up the STDIO transport client:
- Import the necessary modules from the
@ai-sdk/mcpand@modelcontextprotocol/clientpackages. - Create a new instance of
StdioClientTransport, passing the command and arguments to run the MCP server. - Call
createMCPClientwith the transport instance to create the MCP client and establish the connection.clientNameis optional; it's the name the client reports to the server.
Older tutorials, and the AI SDK docs as of October 2026, import the same transport from @modelcontextprotocol/sdk/client/stdio.js, version 1 of the MCP TypeScript SDK, which still works.
Note
The transport doesn't pass your whole environment to the server process: only a few variables, such as HOME, PATH and USER (on Windows, their equivalents).
If the server needs another one, like KUBECONFIG or a proxy setting for npx, add it with the transport's env option, which is merged with those defaults, for example env: {KUBECONFIG: process.env.KUBECONFIG}.
The AI SDK also ships its own stdio transport, Experimental_StdioMCPTransport in @ai-sdk/mcp/mcp-stdio.
Before the classic handshake, it probes the server for the new stateless protocol revision (2026-07-28) and gives up on that probe after one second.
In my tests with v2.0.74, npx -y kubernetes-mcp-server@latest took longer than that to start the server, and the connection failed with a duplicate "initialize" received error.
It connected once I set protocolVersionDiscovery: false in createMCPClient, or when the server started quickly from a local install.
The MCP SDK transport above uses the classic handshake from the start, so I stick with it.
Setting up a Streamable HTTP transport client
The Streamable HTTP transport method allows us to connect to a remote MCP server over HTTP. This is useful when you have an MCP server already running as a service that you want to connect to.
Note
Prior to MCP protocol version 2025-03-26, the SSE (Server-Sent Events) transport was used for remote connections. SSE is now deprecated in favor of Streamable HTTP.
To try it locally, start the Kubernetes MCP Server in HTTP mode in a separate terminal:
npx -y kubernetes-mcp-server@latest --port 8080 --bind-address 127.0.0.1Without --bind-address, the server listens on all network interfaces (0.0.0.0) and doesn't authenticate its clients by default, so anyone who can reach the port can use your cluster with your kubeconfig credentials.
The following code snippet demonstrates how to set up a Streamable HTTP transport client:
import {createMCPClient} from '@ai-sdk/mcp';
const initHttpClient = async () => {
return createMCPClient({
clientName: 'blog.marcnuri.com',
transport: {
type: 'http',
url: 'http://localhost:8080/mcp'
}
});
};In this fragment, we're creating an MCP client that connects to a local server running on port 8080 using the Streamable HTTP protocol. This approach is more suitable for production environments or when you have a dedicated MCP server running.
Note
In production environments, your URL should point to the actual address of your MCP server (which should also be HTTPS-enabled).
The transport also accepts headers (for example, an Authorization header) and an OAuth authProvider.
Since AI SDK 7, it rejects HTTP redirects by default; set redirect: 'follow' if your server relies on them.
These are the key steps to set up the Streamable HTTP transport client:
- Import the necessary module from the
@ai-sdk/mcppackage. - Call
createMCPClientwith the transport configuration, specifying the type ashttpand providing the URL of the MCP server. - The function returns an MCP client instance that can be used to interact with the remote server.
Notice how the HTTP-based transport is much simpler to set up than the STDIO-based transport: the AI SDK includes it, so you don't need the MCP SDK.
Setting up the AI SDK provider and model
Once we have the MCP client set up, we need to configure the AI SDK provider and model that will use the MCP server's tools. The AI SDK has a long list of supported providers; in this case, we will use the community Ollama provider to run a model locally.
The following code snippet demonstrates how to set up the Ollama provider with the Qwen3 4B model:
import {ollama} from 'ollama-ai-provider-v2';
const model = ollama('qwen3:4b-instruct');In this code, we're using the default instance of the Ollama provider and specifying the model we want to use:
- The default
ollamainstance connects to a local Ollama server athttp://127.0.0.1:11434/api. If yours runs elsewhere, create your own instance withcreateOllama({baseURL: '...'}). - The
ollamafunction is then called with the model name to create a model instance that can be used for generating text.
The model must support tool calling, and small models vary a lot in how well they do it. I wrote about this in Giving Superpowers to Small Language Models with MCP.
Tip
Switching to a hosted model only changes these two lines.
For example, with the Google provider (@ai-sdk/google), you'd import google and create the model with google('<model-id>'), reading the API key from the GOOGLE_GENERATIVE_AI_API_KEY environment variable.
In AI SDK 7, the Google provider's createGoogleGenerativeAI function is now createGoogle.
Prompting the model with generateText
Now that we've set up all the necessary components, we can put the pieces together to prompt the AI model and let it interact with the MCP server's tools.
In this example, we are going to generate text by asking the model to list all Kubernetes pods in a Markdown table format.
The following code snippet shows the relevant parts of the pipeline:
import {generateText, isStepCount} from 'ai';
import {ollama} from 'ollama-ai-provider-v2';
const assistant = async () => {
const mcpClient = await initStdioClient();
try {
const tools = await mcpClient.tools();
const model = ollama('qwen3:4b-instruct');
const result = await generateText({
model,
tools,
stopWhen: isStepCount(10),
messages: [{
role: 'user',
content: 'List all pods in my cluster and output their name and namespace as a Markdown table'
}]
});
console.log(result.text);
} finally {
await mcpClient.close();
}
};
assistant().catch(err => {
console.error('Error:', err);
});In this code, we create an assistant that connects the Qwen3 4B model to the Kubernetes MCP server tools and perform a simple prompt.
These are the key components of this example:
- We first initialize the STDIO client and retrieve the available tools by calling
mcpClient.tools(). This lists every tool the server offers (20 with the Kubernetes MCP Server's default toolsets) and converts them to AI SDK tools. - We then call the
generateTextfunction with the following parameters:model: The model instance we created earlier.tools: The tools available from the MCP server client.stopWhen: A condition that determines when to stop the generation loop. We useisStepCount(10)to limit the model to a maximum of 10 steps.
The default forgenerateTextis a single step: the AI SDK would run the tool the model asks for, but the model wouldn't get another step to turn the result into an answer, soresult.textwould be empty. Setting the value to more than 1 ensures the model can perform the initial prompt and subsequent prompts with the tools execution results.messages: An array of messages that represent the conversation history. In this case, we provide a single user message asking the model to list all pods in the cluster and format the output as a Markdown table.
- Finally, we log the generated text to the console and close the client in a
finallyblock, so the server process stops even if the generation fails.
The output of this invocation will be similar to:
| NAMESPACE | NAME |
|----------|------|
| default | nginx-6f5fc479d9-j25wp |
| default | nginx-6f5fc479d9-vq4r9 |
| kube-system | coredns-668d6bf9bc-2q9pp |
| kube-system | coredns-668d6bf9bc-r2jg2 |
| kube-system | etcd-ai-sdk-refresh-control-plane |
| kube-system | kindnet-zncdz |
| kube-system | kube-apiserver-ai-sdk-refresh-control-plane |
| kube-system | kube-controller-manager-ai-sdk-refresh-control-plane |
| kube-system | kube-proxy-rpsdh |
| kube-system | kube-scheduler-ai-sdk-refresh-control-plane |
| local-path-storage | local-path-provisioner-7dc846544d-8xlh6 |result.text only holds the model's final answer.
result.steps contains every step of the loop, including the tool calls and their results, which is where you can check what the model actually did.
That check matters with a small model.
In my runs, Qwen3 4B always called the pods_list tool, but whenever I asked for specific columns, it added a status.phase=Running field selector I hadn't asked for, which would have hidden any pending or failing pod.
When I asked for every column instead, it called the tool without filters, but copied a pod IP wrong while rewriting the 13-column table.
On my laptop, each run took between 25 and 40 seconds.
Limiting the tools the model can use
mcpClient.tools() hands the model every tool the server exposes, including destructive ones like pods_delete and resources_delete.
The simplest way to keep a model away from them is on the server side: the Kubernetes MCP Server's --read-only flag exposes only the tools annotated as read-only.
Read-only prevents changes, not reads: tools like resources_get can still return Secrets, which then reach the model (and its provider, if you use a hosted one); the server's denied_resources setting can keep them out.
args: ['-y', 'kubernetes-mcp-server@latest', '--read-only']With this flag, the client gets 14 tools instead of 20.
On the client side, the AI SDK exposes each tool's MCP tool annotations in its metadata, and its toolApproval option lets you require user approval for the tools that aren't marked read-only.
Annotations are hints from the server, though: only rely on them for servers you trust, since a server can mark a destructive tool as read-only and skip the approval.
Conclusion
In this post, I showed you how to connect to an MCP server from JavaScript using the Vercel AI SDK.
We learned how to set up both STDIO and Streamable HTTP transport clients, configure a local model through the Ollama provider, and prompt the model using the generateText function to generate useful output.
This modular architecture illustrates the flexibility of the SDK: swapping the transport or the model provider only changes a few lines, and the rest of the code stays the same. By following these steps, you can integrate your JavaScript applications with an MCP server and let LLMs interact with external tools and systems.
As the MCP ecosystem matures, the same MCP servers work from the AI SDK, from LangChain.js, or from any other MCP client.
You can find the source code for this post on GitHub.
You Might Also Like
- Introduction to the Model Context Protocol (MCP): The Future of AI Integration
- LangChain.js MCP Adapters: Connect Agents to MCP Servers
- LangChain4j MCP Client: Connect Java to an MCP Server
- Giving Superpowers to Small Language Models with Model Context Protocol (MCP)
- MCP Tool Annotations: Every Hint and Its Default Value
