Services

The services of the MCPServer app: what it can do, for the rest of the platform.

The services know nothing of the Model Context Protocol. The MCPServer’s tools (smarter.apps.mcpserver.protocol.tools) are thin adapters of them, and any other part of the platform, e.g. the web console, a management command or another app, can call them too. Import them from this package, which names the app’s capabilities, rather than from its modules.

The Smarter Project’s repositories, documentation and blog, from the platform’s own index of them:

  • repositories: the repositories and the blog that are indexed.

  • indexing: indexes them from GitHub and from WordPress, every hour, and queues one that is not indexed yet.

  • search: searches their documentation, code and posts.

  • reading: reads a file, a folder, a post, or a repository’s overview.

  • manifests: the JSON Schema and example manifest of each resource kind, and the platform’s own validation of a manifest.

  • github: searches and reads GitHub issues, and opens issues and pull requests as the caller.

This platform’s operations, for its staff:

  • status: the platform’s health now, and the cluster as the cloud provider sees it.

  • logs: the last 72 hours of the pod logs.

  • helpdesk: one user’s account and problems, and the check of a reply to them.

  • acting_user: who is chatting with an LLMClient, signed by the platform for its own staff-only endpoint. The prompt api and the MCPClient app use it.

A service raises SmarterMCPServerServiceError, or its subclass SmarterMCPServerNotFoundError, with a message written for the caller.

Note

Experimental. The MCPServer was designed and coded by Claude Code (Anthropic’s Claude Opus 5.5), with Lawrence McDaniel as co-author. It is experimental, and will be documented.

exception smarter.apps.mcpserver.services.SmarterMCPServerNotFoundError(message, suggestions)[source]

Bases: SmarterMCPServerServiceError

A file, folder or blog post that was asked for does not exist, with what the caller may have meant.

Parameters:
  • message (str) – What was not found, and, when there are suggestions, “Did you mean: …?”.

  • suggestions (list[str]) – The paths that the caller may have meant, best first, or an empty list.

__init__(message, suggestions)[source]

Keep the suggestions, so that the caller can say how to find the item when there are none.

Parameters:
  • message (str) – What was not found, with the suggestions, if any.

  • suggestions (list[str]) – The paths that the caller may have meant.

exception smarter.apps.mcpserver.services.SmarterMCPServerServiceError(message='')[source]

Bases: SmarterMCPServerException

A service could not do what it was asked, e.g. a file does not exist, or a repository is not indexed yet.

The services of smarter.apps.mcpserver.services raise it, with a message that tells the caller what was wrong, and what to do instead, e.g. “Did you mean: …?”. They know nothing of MCP: smarter.apps.mcpserver.protocol.tools.public.PublicTool.call() and smarter.apps.mcpserver.protocol.tools.staff.StaffTool.call() turn it into a SmarterMCPServerToolError, which the LLM reads as the tool’s result.

property get_formatted_err_message: str

Return the title of the error, as the platform’s error responses show it.

Returns:

"Smarter MCPServer service error".

smarter.apps.mcpserver.services.acting_user(user)[source]

Make a user the acting user of the MCP server requests that the block makes.

The value is a context variable, so it follows the code into asgiref.sync.async_to_sync() and asgiref.sync.sync_to_async(), and is restored when the block ends.

Example usage:

with acting_user(request.user):
    response = handler(user_profile, prompt, data)
Parameters:

user (object | None) – The user. Anything that is not a User, e.g. AnonymousUser or None, means nobody in particular.

Yields:

Nothing.

smarter.apps.mcpserver.services.acting_user_headers(url, user)[source]

Return the acting user header to send to an MCP server, if it is this platform’s staff-only endpoint.

Parameters:
  • url (str | None) – The MCP server’s url.

  • user (User | None) – The acting user, usually the current_acting_user() when the connection was created, or None for nobody in particular.

Return type:

dict[str, str]

Returns:

{ACTING_USER_HEADER: <signed value>} for this platform’s own staff-only endpoint, or an empty dict for any other MCP server, which never learns who is chatting.

smarter.apps.mcpserver.services.check_reply(reply, requester_email=None)[source]

Find everything in a draft reply that breaks the reply rules.

Parameters:
  • reply (str) – The draft reply to the user.

  • requester_email (str | None) – The requester’s own email address, which the reply may contain, or None.

Return type:

list[ReplyFinding]

Returns:

The findings, those of the Guardrail detectors first, at most MAX_FINDINGS, or an empty list if none of the rules finds anything.

smarter.apps.mcpserver.services.current_acting_user()[source]

Return the acting user that acting_user() set, if any.

Return type:

User | None

Returns:

The user, or None outside an acting_user() block, or for an anonymous user.

smarter.apps.mcpserver.services.eks_status()[source]

Read the cluster’s health from the cloud provider, and the recent AWS events of its region.

Side effects: read-only calls to the EKS api (DescribeCluster, ListNodegroups, DescribeNodegroup, ListAddons, DescribeAddon, DescribeAddonVersions and ListInsights), and up to two HTTPS GETs of AWS status feeds, which are cached for a minute.

Return type:

EksStatus

Returns:

The cluster’s health, and the region’s AWS events.

Raises:
smarter.apps.mcpserver.services.get_repository(name)[source]

Return one of SMARTER_PROJECT_REPOSITORIES, by name.

Parameters:

name (str) – The repository’s name, e.g. smarter-cli.

Return type:

ProjectRepository | None

Returns:

The repository, or None if there is no such repository.

smarter.apps.mcpserver.services.index_blog(force=False, blog=None)[source]

Bring the blog’s index up to date with its published posts.

If the posts’ fingerprint is already indexed, only a previous error is cleared. Two posts with the same path are indexed once, the newer one.

A failure never raises: the index keeps its posts, and stays ready if it was, so that the tools can go on answering from them, and the failure is recorded in its error.

Side effects: HTTP requests to the blog (see fetch_posts()), database writes (the index’s row is created if it does not exist, and see store_files()), and a signal: mcpserver_repository_indexed when the posts are replaced, whose receiver makes the cached tool results stale, or mcpserver_repository_index_failed.

Parameters:
Return type:

RepositoryIndex

Returns:

The blog’s index. If indexing fails, its error says why, and an existing index is kept.

Example usage:

index = index_blog()
index.file_count  # the number of posts
smarter.apps.mcpserver.services.index_repository(name, force=False)[source]

Bring a repository’s index up to date with the latest commit of its branch.

If the latest commit is already indexed, only a previous error is cleared. Otherwise the archive is downloaded to a temporary file, which is deleted afterwards, and its files replace the index’s (see the module’s description for the steps).

A failure never raises: the index keeps its files, and stays ready if it was, so that the tools can go on answering from the previous commit, and the failure is recorded in its error.

Side effects: HTTP requests to GitHub (see latest_commit_sha() and download_archive()), a temporary file, database writes (the index’s row is created if it does not exist, and see store_files()), and a signal: mcpserver_repository_indexed when the files are replaced, whose receiver makes the cached tool results stale, or mcpserver_repository_index_failed.

Parameters:
  • name (str) – The repository’s name, e.g. smarter-cli.

  • force (bool) – Index the repository even if its latest commit is already indexed.

Return type:

RepositoryIndex

Returns:

The repository’s index. If indexing fails, its error says why, and an existing index is kept.

Raises:

SmarterMCPServerIndexError – If there is no such repository.

Example usage:

index = index_repository("smarter-cli")
index.status  # 'ready'
smarter.apps.mcpserver.services.is_staff_endpoint_url(url)[source]

Return whether a url is that of this platform’s own staff-only endpoint.

The scheme, host, port and path must all match staff_endpoint_url(), with or without a trailing slash, so that a server elsewhere whose path looks the same never gets the header.

Parameters:

url (str | None) – An MCPClient’s endpoint url.

Return type:

bool

Returns:

True if the url is this platform’s staff-only endpoint.

smarter.apps.mcpserver.services.list_files(repository, folder=None)[source]

List the folders and indexed files directly in a folder of a repository.

Side effects: database queries, and, for a repository that is not indexed yet, a Celery task queued to index it.

Parameters:
  • repository (ProjectRepository) – The repository.

  • folder (str | None) – The folder, e.g. smarter/smarter/apps, or None for the top level.

Return type:

FolderListing

Returns:

The folder’s entries and GitHub url.

Raises:
smarter.apps.mcpserver.services.log_errors(minutes, min_level, components=())[source]

Return the errors, or the warnings and errors, of the last minutes, grouped by their signature.

Side effects: one HTTP GET to Loki’s query_range.

Parameters:
  • minutes (int) – The period, in minutes, up to now.

  • min_level (LogLevel) – LogLevel.ERROR for errors and critical lines, or LogLevel.WARNING to include warnings.

  • components (tuple[str, ...]) – The components to read, or an empty tuple for all of them.

Return type:

LogErrors

Returns:

The number of lines, whether they were cut at SUMMARY_LINES, and the groups.

Raises:
smarter.apps.mcpserver.services.log_sources(minutes)[source]

Return the components and pods that wrote to the logs in the last minutes.

Side effects: two HTTP GETs to Loki, for the values of the component and pod labels.

Parameters:

minutes (int) – The period, in minutes, up to now.

Return type:

LogSources

Returns:

The components, the components that wrote nothing, and the pods.

Raises:
smarter.apps.mcpserver.services.platform_status(minutes)[source]

Read the platform’s health now, and what looks wrong.

Side effects: five kubectl get calls, a SELECT 1 and two SHOW queries on the database, a value written to and deleted from the cache, a ping of the Celery workers, a connection to the Celery broker, and up to five HTTPS GETs of public status pages, which are cached for a minute. Nothing in the cluster, the database or the queues is changed.

Parameters:

minutes (int) – How far back to read Kubernetes warning events.

Return type:

PlatformStatus

Returns:

Everything that was read, and the observations. A part that cannot be read is named in the observations, and the others are still read.

smarter.apps.mcpserver.services.project_blog()[source]

Return SMARTER_PROJECT_BLOG.

Every module reads the blog through this function, so that tests can replace it with a throwaway blog, whose index does not collide with the real one.

Return type:

ProjectBlog

Returns:

The blog.

smarter.apps.mcpserver.services.project_repositories()[source]

Return SMARTER_PROJECT_REPOSITORIES.

Every module reads the repositories through this function, so that tests can replace them with throwaway repositories, whose indexes do not collide with the real ones.

Return type:

tuple[ProjectRepository, ...]

Returns:

The repositories, in the order that the MCP server lists them.

smarter.apps.mcpserver.services.read_blog_post(path, start_line=1, end_line=None)[source]

Read a post of the blog, or a range of its lines, by its path or its url.

Side effects: database queries, and, if the blog is not indexed yet, a Celery task queued to index it.

Parameters:
  • path (str) – The post’s path, e.g. 2026/10/26/release-smarter-api-v0-6, or its url.

  • start_line (int) – The first line to read, from 1.

  • end_line (int | None) – The last line to read, or None to read as many as read_lines() allows.

Return type:

PostLines

Returns:

The lines, and the post’s url.

Raises:
smarter.apps.mcpserver.services.read_file(repository, path, start_line=1, end_line=None)[source]

Read a file of a repository, or a range of its lines.

Side effects: database queries, and, for a repository that is not indexed yet, a Celery task queued to index it.

Parameters:
  • repository (ProjectRepository) – The repository.

  • path (str) – The file’s path in the repository, e.g. README.md. A leading slash is ignored.

  • start_line (int) – The first line to read, from 1.

  • end_line (int | None) – The last line to read, or None to read as many as read_lines() allows.

Return type:

FileLines

Returns:

The lines, and the file’s GitHub url and documentation page.

Raises:
smarter.apps.mcpserver.services.repositories_state(repositories)[source]

Return whether each repository is indexed, and queue those that are not for indexing.

Side effects: one database query, and a Celery task queued for each repository that is not indexed yet (see queue_indexing()).

Parameters:

repositories (Sequence[ProjectRepository]) – The repositories, in the order to return them.

Return type:

list[RepositoryState]

Returns:

One state per repository, in the same order.

smarter.apps.mcpserver.services.repository_overview(repository)[source]

Describe a repository: its indexed commit, its numbers of files, its top-level folders and files, and its README.

Side effects: database queries, and, for a repository that is not indexed yet, a Celery task queued to index it.

Parameters:

repository (ProjectRepository) – The repository.

Return type:

RepositoryOverview

Returns:

The overview.

Raises:

SmarterMCPServerServiceError – If the repository is not indexed yet.

smarter.apps.mcpserver.services.search_blog(query, limit)[source]

Search the posts of The Smarter Project’s blog.

Side effects: database queries, and, if the blog is not indexed yet, a Celery task queued to index it.

Parameters:
  • query (str) – The query, e.g. caching.

  • limit (int) – The maximum number of hits.

Return type:

list[SearchHit]

Returns:

The passages of the posts, best first.

Raises:

SmarterMCPServerServiceError – If the blog is not indexed yet.

smarter.apps.mcpserver.services.search_code(repositories, query, limit, path=None)[source]

Search the source code and configuration of the repositories.

Side effects: see search_repositories().

Parameters:
  • repositories (Sequence[ProjectRepository]) – The repositories to search.

  • query (str) – The query, e.g. verify_custom_domain.

  • limit (int) – The maximum number of hits.

  • path (str | None) – Only files under this folder, e.g. smarter/smarter/apps/llmclient, or None.

Return type:

SearchResults

Returns:

The files, best first, with their matching lines, and the repositories that were not searched.

Raises:

SmarterMCPServerServiceError – If none of the repositories is indexed yet.

smarter.apps.mcpserver.services.search_documentation(repositories, query, limit)[source]

Search the documentation of the repositories: the Sphinx documentation, READMEs and other prose.

Side effects: see search_repositories().

Parameters:
  • repositories (Sequence[ProjectRepository]) – The repositories to search.

  • query (str) – The query, e.g. custom domain verification.

  • limit (int) – The maximum number of hits.

Return type:

SearchResults

Returns:

The passages, best first, and the repositories that were not searched.

Raises:

SmarterMCPServerServiceError – If none of the repositories is indexed yet.

Example usage:

results = search_documentation([get_repository("smarter")], "custom domain verification", limit=8)
results.hits[0].documentation_url
smarter.apps.mcpserver.services.search_logs(minutes, limit, components=(), pod=None, contains=(), pattern=None, min_level=None, oldest_first=False)[source]

Return the lines of the last minutes that match every filter.

Side effects: one HTTP GET to Loki’s query_range.

Parameters:
  • minutes (int) – The period, in minutes, up to now.

  • limit (int) – The most lines to return.

  • components (tuple[str, ...]) – The components to read, or an empty tuple for all of them.

  • pod (str | None) – One pod’s name, or None for every pod of the components.

  • contains (tuple[str, ...]) – Texts that every line must contain, case insensitively.

  • pattern (str | None) – A regular expression, in RE2 syntax, that every line must match, or None.

  • min_level (LogLevel | None) – The least severe level to return, or None for lines of every level.

  • oldest_first (bool) – Whether to return the oldest lines of the period first, rather than the newest.

Return type:

list[LogLine]

Returns:

The lines, newest first unless oldest_first.

Raises:
smarter.apps.mcpserver.services.staff_endpoint_url()[source]

Return the url of this platform’s staff-only endpoint.

It is reversed against the platform’s urlconf, because a prompt may run on a deployed LLMClient’s host, whose urlconf has no MCPServer endpoints.

Return type:

str

Returns:

The url, e.g. https://alpha.platform.example.com/api/v1/mcp/smarter-operations/.

smarter.apps.mcpserver.services.support_snapshot(email, days=7)[source]

Gather what the platform knows about one user’s problem.

The user is found by their email address, case insensitively. Their failed sign-ins are searched for even when no user has the address, because someone may be trying to sign in with an address that is mistyped, or not registered.

Side effects: queries of the users, profiles, social sign-ins and journal, one query of the log store, and, on a hosted platform, a call to the Account Manager. Nothing is written.

Parameters:
  • email (str) – The email address of the person who asked for help, as staff verified it.

  • days (int) – How far back to read the journal, in days.

Return type:

SupportSnapshot

Returns:

The snapshot. A part that cannot be read is named in its errors.

smarter.apps.mcpserver.services.validate_manifest_text(text)[source]

Validate a manifest that the LLM wrote, against the Pydantic model of its kind, without saving it.

The checks that make requests, e.g. that an image url responds, are skipped.

Parameters:

text (str) – The manifest, in YAML or JSON.

Return type:

CliApiValidation

Returns:

Whether it is valid, and its errors.

Raises:

SmarterMCPServerServiceError – If it is too long, or is not YAML.

smarter.apps.mcpserver.services.verify_acting_user(value)[source]

Verify a signed value of ACTING_USER_HEADER.

Parameters:

value (str) – The header’s value.

Return type:

ActingUser | None

Returns:

The acting user, or None if the value was not signed by this platform with SALT, has expired, or is not an ActingUser.