guides

Stateful MCP servers: the pattern and a shortcut

Most MCP servers wrap an API and forget everything between calls. A stateful server gives your agent persistent memory. Here is the pattern, the decisions you will face, and a ready-made implementation.

Most MCP servers are stateless

A typical MCP server wraps an external API. The client sends a tool call. The server transforms the request, calls the API, and returns the result. Each call is independent. The server holds no data between calls.

This works for tools (search, email, database queries). It does not work for memory. If the agent learns something in call five, call six does not know about it. Nothing accumulates between sessions.

What state changes

A stateful server owns a store. Tool calls read from it and write to it. The store persists after the session ends. The next session reads it. The client's context window is no longer the only place knowledge lives.

The store can be anything: a database, an in-memory map, a folder of files. Files are the simplest choice for agent knowledge. The LLM already knows how to read and write text. You can inspect the files in your editor. You can commit them to git.

The minimal design

Start with four tools over a root folder: read_file, write_file, list_dir, and grep. Add resource registration so the client can attach a file directly without a tool call. Guard all paths against traversal.

This sketch shows the shape, not a working server. A production implementation needs proper MCP protocol handling, error responses, and content-type negotiation. Four tool handlers and a path guard give you a stateful server.

sketch.py (conceptual, not runnable)
class StatefulMCPServer:
def __init__(self, root_path: Path):
self.root = root_path.resolve()
# Tools
def read_file(self, path: str) -> str:
safe = self._resolve(path)
return safe.read_text(encoding="utf-8")
def write_file(self, path: str, content: str):
safe = self._resolve(path)
safe.parent.mkdir(parents=True, exist_ok=True)
safe.write_text(content, encoding="utf-8")
def list_dir(self, path: str = '.') -> list[str]:
return [p.name for p in self._resolve(path).iterdir()]
def grep(self, pattern: str, path: str = '.') -> list[dict]:
# walk files under path, return matching lines
...
# Resources: expose each file as gcontext://<path>
def register_resources(self):
for f in self.root.rglob('*'):
if f.is_file():
self.add_resource(f'gcontext://{f.relative_to(self.root)}')
# Path guard
def _resolve(self, path: str) -> Path:
target = (self.root / path).resolve()
if not target.is_relative_to(self.root):
raise ValueError("path traversal blocked")
return target

Design decisions that matter

Scope the root. One folder per instance. The server sees nothing outside it. This is the security boundary and the portability boundary. Copy the folder to share the state.

Expose files as resources. MCP resources let the client attach a file to the prompt without a tool call. A resource URI like gcontext://modules/deploy/index.md gives the client direct read access.

Keep secrets out of state. API keys and tokens live in a secrets.env file that the server never exposes through tools or resources. The state folder is safe to commit to git.

What you build next anyway

  • Script execution. A tool like run_script executes a Python file from the state folder with access to dependencies and injected secrets.
  • Secret injection. The server loads secrets from secrets.env and passes them as environment variables to the script process.
  • Dependency management. Each connection declares its dependencies in connection.yaml. The server installs them into an isolated environment.
  • An install story. An agent registry lets you install a pre-built agent with one command.

Each of these is a natural extension of the file-based pattern. You can build them yourself, or you can use a server that already has them.

Or install the ready-made version

gcontext is this server, plus the conventions, script execution, secret injection, dependency management, and an agent registry. Install it, create an instance, start the server, and connect your client.

Your agent now has read_file, write_file, list_dir, grep, run_script, and run_adhoc_script. State lives in the instance folder. Push it to git. Share it with teammates.

install, start, and connect
$ uv tool install gcontext-ai
$ gcontext init my-agent
$ gcontext up my-agent
$ claude mcp add --transport http my-agent http://127.0.0.1:4242/mcp

Questions

Can an MCP server keep state between calls?

Yes. The server holds a reference to persistent storage. Tool calls read and write it. The MCP protocol does not prevent this. The server process stays running between calls, so any store it references (filesystem, database, in-memory map) persists across the session.

Where should MCP server state live?

On the filesystem, in a database, or in memory. Files are the simplest option for agent knowledge because the LLM can read and write them directly. A folder of markdown and YAML files is inspectable, diffable, and version-controllable with git.

Is a database better than files for MCP state?

For structured entities with relationships, yes. For agent working notes, learnings, and project context, files are simpler and more inspectable. You can open them in any editor, review them in pull requests, and grep them from a terminal.

Related

What is agent state?
Definition and management patterns
AI agent memory
Four approaches to persistent agent memory
Share context across tools
One state folder for multiple AI clients
Agent registry
Install and share agents with one command
Skip the build, keep the pattern

gcontext is a stateful MCP server with conventions, script execution, and an agent registry built in.

View on GitHubAgent memory approaches