Pedal Assist Coding logo

Pedal Assist Coding

Archives
RSS Feed
CommonsWare
Log in
Subscribe
September 3, 2026

Thursdays with Koog: MCP

In Which the Author Wonders When Standard I/O Became a "Server"

When working with coding agents, you will from time to time see sites that tell you to "add an MCP server". Here, MCP is the Model Context Protocol, a specification from Anthropic. MCP is a way to extend agent harnesses with new tools, where the protocol itself describes how the harness discovers available tools and how it invokes them.

Developers creating agent harnesses like Knosh or otherwise interacting with LLMs though libraries like Koog have two main angles for considering MCP:

  • Creating code that can talk to an MCP server, such as allowing Knosh to invoke MCP-supplied tools
  • Creating the actual MCP servers themselves

Knosh 0.4.0 handles the former, by way of a Koog-supplied MCP bridge, surfacing MCP-supplied tools into Koog's own tool system.


Knosh's config file (by default at ~/.config/knosh/knosh.json) now supports an mcp object:

{
    "mcp": {
        "composables": {
            "type": "remote",
            "url": "https://composables.com/mcp"
        }
    }

    // rest of configuration
}

That gets turned into a Map tying string identifiers (e.g., composables) to details of the MCP server that should be made available to agents.

MCP supports two types of servers:

  • Remote servers, where communications happen over TCP/IP
  • Local "servers", where communications happen over standard I/O

The data being exchanged and the general JSON structure of that data is the same in both cases. The difference is mostly in the transport layer. However, that difference requires dedicated configuration structures, as a remote server requires a URL, while a local server requires the command to run to start the server. In Knosh's case, the JSON configuration eventually turns into McpServerEntry instances:

/**
 * A resolved MCP server, built from a [KnoshConfig.mcp] entry. Either a [LocalMcpServerEntry] (spawned as a child
 * process) or a [RemoteMcpServerEntry] (reached over MCP Streamable HTTP).
 */
public sealed interface McpServerEntry {
  /** The in-Knosh server identifier (the key under `mcp` in `knosh.json`). */
  public val name: String

  /** The server type: `"local"` or `"remote"`. */
  public val type: String

  /** Whether this server is active. */
  public val enabled: Boolean

  /** The server's timeout in seconds, or `null` for no configured timeout. */
  public val timeout: Int?

  /** Whether a startup failure is fatal. */
  public val required: Boolean
}

/**
 * A resolved local MCP server, spawned as a child process.
 *
 * @property name the in-Knosh server identifier (the key under `mcp` in `knosh.json`)
 * @property command the server's resolved argv (from [McpServerConfig.effectiveCommand])
 * @property enabled whether this server is active
 * @property environment environment variables to pass to the server process
 * @property timeout the server's timeout in seconds, or `null` for no configured timeout
 * @property required whether a startup failure is fatal
 */
@Poko
public class LocalMcpServerEntry(
  public override val name: String,
  public val command: List<String>,
  public override val enabled: Boolean,
  public val environment: Map<String, String>,
  public override val timeout: Int?,
  public override val required: Boolean,
) : McpServerEntry {
  public override val type: String = "local"
}

/**
 * A resolved remote MCP server, reached over MCP Streamable HTTP.
 *
 * @property name the in-Knosh server identifier (the key under `mcp` in `knosh.json`)
 * @property url the remote server's URL
 * @property enabled whether this server is active
 * @property headers HTTP headers sent to the remote server, raw and uninterpolated (see `McpHeaderResolver`)
 * @property timeout the server's timeout in seconds, or `null` for no configured timeout
 * @property required whether a startup failure is fatal
 */
@Poko
public class RemoteMcpServerEntry(
  public override val name: String,
  public val url: String,
  public override val enabled: Boolean,
  public val headers: Map<String, String>,
  public override val timeout: Int?,
  public override val required: Boolean,
) : McpServerEntry {
  public override val type: String = "remote"
}

(Knosh links in this issue point to 0.4.0, released a few days ago)

Agent frontmatter can also declare mcp structures, either for MCP servers to be uniquely used by that agent, or to enable/disable servers defined in the main Knosh configuration. Knosh has a bunch of logic to determine which McpServerEntry objects are relevant for a given run, based on the chosen agent and the overall configuration.

The primary MCP client support is not directly through Koog, but rather through the MCP Kotlin SDK, which JetBrains helps to maintain. That SDK provides StdioClientTransport and StreamableHttpClientTransport for talking to local and remote servers, respectively. Knosh creates an instance of those based on what type of MCP server was configured and its details, such as the remote server URL. Those transports get wrapped into a Client instance, which is the in-app representation of the MCP server itself.

Koog then layers atop the MCP Kotlin SDK. It offers McpToolRegistryProvider.fromClient() that takes a Client, finds out what MCP-style tools it offers, and wraps those in Koog Tool instances, bundling them all up into a ToolRegistry. Knosh uses that in its discoverMcpTools() function:

/** Fetches [entry]'s tools from [client]. Production default for [McpToolDiscoverer], shared by local and remote. */
internal suspend fun discoverMcpTools(client: Client, entry: McpServerEntry): ToolRegistry =
  McpToolRegistryProvider.fromClient(
    mcpClient = client,
    serverInfo =
      McpServerInfo(
        url = (entry as? RemoteMcpServerEntry)?.url,
        command = (entry as? LocalMcpServerEntry)?.command?.firstOrNull(),
      ),
  )

After going through some permission checks, those tools wind up being blended with Knosh's own tools to create the overall ToolRegistry that gets passed to the Koog AIAgent that will process our prompt and return the LLM response.

The vast majority of Knosh's MCP-related code is tied up in handling the configuration and using that to create the MCP Client objects. After that, Koog's Tool abstraction, and its supplied way of mapping MCP tools to Tool instances, saves Knosh from having to do that work itself.

The net is that you can add MCP servers to Knosh, globally or per-agent, and then be able to use those servers' tools alongside the ones Knosh provides intrinsically.


Knosh was not the only one of my projects to get an update recently. koverGate, the Gradle task to help agents interpret Kover reports, is up to 0.7.0. This adds a bunch of new features that help agents make effective use of coverage reports, from sorting results by leverage (so the agent knows where to focus its efforts to boost coverage) to delta-tracking (so the agent knows how much its last round of work affected coverage).

Next week, I will look at projects that extend Koog, such as for allowing on-device inference using mobile local models, as well as some Koog alternatives.

Don't miss what's next. Subscribe to Pedal Assist Coding:
Older → You Got mlx-serve'd!

Add a comment:

Posting this comment will subscribe you to this newsletter with the email address you enter.
Bluesky
androiddev.social
commonsware.com
Powered by Buttondown, the easiest way to start and grow your newsletter.