{"id":26641166,"url":"https://github.com/ondrsh/mcp4k","last_synced_at":"2025-04-10T20:44:48.437Z","repository":{"id":269280009,"uuid":"906153251","full_name":"ondrsh/mcp4k","owner":"ondrsh","description":"Compiler-driven MCP framework for Kotlin Multiplatform","archived":false,"fork":false,"pushed_at":"2025-03-20T12:14:40.000Z","size":568,"stargazers_count":39,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-20T13:44:13.024Z","etag":null,"topics":["agentic-ai","agents","ai","kmp","kmp-library","llm","mcp","modelcontextprotocol"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ondrsh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-20T09:22:56.000Z","updated_at":"2025-03-20T12:13:16.000Z","dependencies_parsed_at":"2024-12-22T12:23:42.619Z","dependency_job_id":"c606b91a-abdf-4b5f-8e3c-1f0147388be4","html_url":"https://github.com/ondrsh/mcp4k","commit_stats":null,"previous_names":["ondrsh/kmcp"],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ondrsh%2Fmcp4k","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ondrsh%2Fmcp4k/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ondrsh%2Fmcp4k/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ondrsh%2Fmcp4k/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ondrsh","download_url":"https://codeload.github.com/ondrsh/mcp4k/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248294544,"owners_count":21079923,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agentic-ai","agents","ai","kmp","kmp-library","llm","mcp","modelcontextprotocol"],"created_at":"2025-03-24T18:20:00.976Z","updated_at":"2025-04-10T20:44:48.400Z","avatar_url":"https://github.com/ondrsh.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./mcp4k.svg\" alt=\"mcp4k banner\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://central.sonatype.com/search?q=sh.ondr.mcp4k\"\u003e\n    \u003cimg src=\"https://img.shields.io/maven-central/v/sh.ondr.mcp4k/mcp4k-gradle.svg?label=Maven%20Central\u0026color=blue\" alt=\"Maven Central\"/\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://www.apache.org/licenses/LICENSE-2.0\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/License-Apache%202.0-blue.svg?color=blue\" alt=\"License\"/\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cb\u003emcp4k\u003c/b\u003e is a compiler-driven framework for building both \u003cb\u003eclients and servers\u003c/b\u003e using the\n\u003ca href=\"https://modelcontextprotocol.io\"\u003eModel Context Protocol\u003c/a\u003e (MCP) in Kotlin.\nIt implements the vast majority of the MCP specification, including resources, prompts, tools, sampling, and more.\n\nBy annotating your functions with ```@McpTool``` or ```@McpPrompt```,\nmcp4k automatically generates JSON-RPC handlers, schema metadata, and a complete lifecycle framework for you.\n\n---\n\n## Overview\n\n- **Client**: Connects to any MCP server to request prompts, read resources, or invoke tools.\n- **Server**: Exposes resources, prompts, and tools to MCP-compatible clients, handling standard JSON-RPC messages and protocol events.\n- **Transports**: Supports `stdio`, with HTTP-SSE and other transports on the roadmap.\n- **Lifecycle**: Manages initialization, cancellation, sampling, progress tracking, and more.\n\nmcp4k goes beyond simple stubs: it also enforces correct parameter typing at compile time.\nIf you describe a tool parameter incorrectly, you get a compile-time error instead of a runtime mismatch.\n\n---\n\n## Installation\n\nAdd mcp4k to your build:\n\n```kotlin\nplugins {\n  kotlin(\"multiplatform\") version \"2.1.0\" // or kotlin(\"jvm\")\n  kotlin(\"plugin.serialization\") version \"2.1.0\"\n\n  id(\"sh.ondr.mcp4k\") version \"0.3.6\" // \u003c-- Add this\n}\n```\n\n---\n\n## Quick Start\n\n### Create a Simple Server\n\n```kotlin\n/**\n * Reverses an input string\n *\n * @param input The string to be reversed\n */\n@McpTool\nfun reverseString(input: String): ToolContent {\n  return \"Reversed: ${input.reversed()}\".toTextContent()\n}\n\nfun main() = runBlocking {\n  val server = Server.Builder()\n    .withTool(::reverseString)\n    .withTransport(StdioTransport())\n    .build()\n    \n  server.start()\n  \n  // Keep server running \n  while (true) { \n    delay(1000)\n  }\n}\n```\n\nIn this example, your new ```@McpTool``` is exposed via JSON-RPC as ```reverseString```.\nClients can call it by sending ```tools/call``` messages.\n\n---\n\n### Create a Simple Client\n\n```kotlin\nfun main() = runBlocking {\n  val client = Client.Builder()\n    .withClientInfo(\"MyClient\", \"1.0.0\")\n    .withTransport(StdioTransport())\n    .build()\n    \n  // Connect to a MCP server using the supplied transport\n  client.start()\n  client.initialize()\n  \n  // For example, list available tools using pagination\n  val allTools = mutableListOf\u003cTool\u003e()\n  var pageCount = 0\n  \n  client.fetchPagesAsFlow(ListToolsRequest).collect { pageOfTools -\u003e\n    pageCount++\n    allTools += pageOfTools\n  }\n  println(\"Server tools = ${allTools}\")\n}\n```\n\nOnce connected, the client can discover prompts/tools/resources and make calls according to the MCP spec.\nAll boilerplate (capability negotiation, JSON-RPC ID handling, etc.) is handled by mcp4k.\n\n---\n\n## Transport Logging\n\nYou can observe raw incoming/outgoing messages by providing ```withTransportLogger``` lambdas:\n\n```kotlin\nval server = Server.Builder()\n  .withTransport(StdioTransport())\n  .withTransportLogger(\n    logIncoming = { msg -\u003e println(\"SERVER INCOMING: $msg\") },\n    logOutgoing = { msg -\u003e println(\"SERVER OUTGOING: $msg\") },\n  )\n  .build()\n```\n\nBoth ```Server``` and ```Client``` accept this configuration. Super useful for debugging and tests.\n\n---\n\n## Tools\n\n```kotlin\n@JsonSchema @Serializable\nenum class Priority {\n  LOW, NORMAL, HIGH\n}\n\n/**\n * @property title The email's title\n * @property body The email's body\n * @property priority The email's priority\n */\n@JsonSchema @Serializable\ndata class Email(\n  val title: String,\n  val body: String?,\n  val priority: Priority = Priority.NORMAL,\n)\n\n/**\n * Sends an email\n * @param recipients The email addresses of the recipients\n * @param email The email to send\n */\n@McpTool\nfun sendEmail(\n  recipients: List\u003cString\u003e,\n  email: Email,\n) = buildString {\n  append(\"Email sent to ${recipients.joinToString()} with \")\n  append(\"title '${email.title}' and \")\n  append(\"body '${email.body}' and \")\n  append(\"priority ${email.priority}\")\n}.toTextContent()\n```\n\nWhen clients call `tools/list`, they see a JSON schema describing the tool's input:\n\n```json\n{\n  \"type\": \"object\",\n  \"description\": \"Sends an email\",\n  \"properties\": {\n    \"recipients\": {\n      \"type\": \"array\",\n      \"description\": \"The email addresses of the recipients\",\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"email\": {\n      \"type\": \"object\",\n      \"description\": \"The email to send\",\n      \"properties\": {\n        \"title\": {\n          \"type\": \"string\",\n          \"description\": \"The email's title\"\n        },\n        \"body\": {\n          \"type\": \"string\",\n          \"description\": \"The email's body\"\n        },\n        \"priority\": {\n          \"type\": \"string\",\n          \"description\": \"The email's priority\",\n          \"enum\": [\n            \"LOW\",\n            \"NORMAL\",\n            \"HIGH\"\n          ]\n        }\n      },\n      \"required\": [\n        \"title\"\n      ]\n    }\n  },\n  \"required\": [\n    \"recipients\",\n    \"email\"\n  ]\n}\n```\nKDoc parameter descriptions are type-safe and will throw a compile-time error if you specify a non-existing property.\n\nClients can now send a `tools/call` request with a JSON object describing the above schema. Invocation and type-safe deserialization will be handled by mcp4k.\n\n---\n\n## Prompts\n\nAnnotate functions with ```@McpPrompt``` to define parameterized conversation templates:\n\n```kotlin\n@McpPrompt\nfun codeReviewPrompt(code: String) = buildPrompt {\n  user(\"Please review the following code:\")\n  user(\"'''\\n$code\\n'''\")\n}\n```\n\nClients can call ```prompts/get``` to retrieve the underlying messages.\n\n---\n\n## Server Context\n\nIn some cases, you want multiple tools or prompts to share state.\n\nmcp4k allows you to attach a custom **context object** that tools and prompts can reference. For example:\n\n```kotlin\n// 1) Implement your custom context\nclass MyServerContext : ServerContext {\n  var userName: String = \"\"\n}\n\n// 2) A tool function that writes into the context\n@McpTool\nfun Server.setUserName(name: String): ToolContent {\n  getContextAs\u003cMyServerContext\u003e().userName = name\n  return \"Username set to: $name\".toTextContent()\n}\n\n// 3) Another tool that reads from the context\n@McpTool\nfun Server.greetUser(): ToolContent {\n  val name = getContextAs\u003cMyServerContext\u003e().userName\n  if (name.isEmpty()) return \"No user set yet!\".toTextContent()\n  return \"Hello, $name!\".toTextContent()\n}\n\nfun main() = runBlocking {\n  val context = MyServerContext()\n  val server = Server.Builder()\n    .withContext(context) // \u003c-- Provide the context\n    .withTool(Server::setUserName)\n    .withTool(Server::greetUser)\n    .withTransport(StdioTransport())\n    .build()\n  \n  server.start()\n  while(true) {\n    delay(1000)\n  }\n}\n```\n\n1) Pass it in with ```.withContext(MyServerContext())```\n2) Each tool or prompt can access it by calling ```getContextAs()```\n\n---\n\n## Resources\n\nResources are provided by a `ResourceProvider`. You can either create your own `ResourceProvider` or use one of the 2 default implementations:\n\n### DiscreteFileProvider\n\nLet's say you want to expose 2 files:\n- /app/resources/cpp/my_program.h\n- /app/resources/cpp/my_program.cpp\n\nYou would first create the following provider:\n```kotlin\nval fileProvider = DiscreteFileProvider(\n  fileSystem = FileSystem.SYSTEM,\n  rootDir = \"/app/resources\".toPath(),\n  initialFiles = listOf(\n    File(\n      relativePath = \"cpp/my_program.h\",\n      mimeType = \"text/x-c++\",\n    ),\n    File(\n      relativePath = \"cpp/my_program.cpp\",\n      mimeType = \"text/x-c++\",\n    ),\n  )\n)\n```\n\nAnd add it when building the server:\n```kotlin\nval server = Server.Builder()\n  .withResourceProvider(fileProvider)\n  .withTransport(StdioTransport())\n  .build()\n```\n\nA client calling `resources/list` will then receive:\n```json\n{\n  \"resources\": [\n    {\n      \"uri\": \"file://cpp/my_program.h\",\n      \"name\": \"my_program.h\",\n      \"description\": \"File at cpp/my_program.h\",\n      \"mimeType\": \"text/x-c++\"\n    },\n    {\n      \"uri\": \"file://cpp/my_program.cpp\",\n      \"name\": \"my_program.cpp\",\n      \"description\": \"File at cpp/my_program.cpp\",\n      \"mimeType\": \"text/x-c++\"\n    }\n  ]\n}\n```\n\nA client sending a `resources/read` request to fetch the contents of the source file would receive:\n```json\n{\n  \"contents\": [\n    {\n      \"uri\": \"file://cpp/my_program.cpp\",\n      \"mimeType\": \"text/x-c++\",\n      \"text\": \"int main(){}\"\n    }\n  ]\n}\n```\n\nYou can also add or remove files at runtime via\n```kotlin\nfileProvider.addFile(\n  File(\n    relativePath = \"cpp/README.txt\",\n    mimeType = \"text/plain\",\n  )\n)\n\nfileProvider.removeFile(\"cpp/my_program.h\")\n```\n\nBoth `addFile` and `removeFile` will send a `notifications/resources/list_changed` notification.\n\n\u003cbr\u003e\n\nWhen making changes to a file, always call\n```kotlin\nfileProvider.onResourceChange(\"cpp/my_program.h\")\n```\n\nIf (and only if) the client subscribed to this resource, this will send a `notifications/resources/updated` notification to the client.\n\n\u003cbr\u003e\n\n### TemplateFileProvider\n\nIf you want to expose a whole directory, you can do:\n```kotlin\nval templateFileProvider = TemplateFileProvider(\n  fileSystem = FileSystem.SYSTEM,\n  rootDir = \"/app/resources\".toPath(),\n)\n```\n\nA client calling `resources/templates/list` will receive:\n```json\n{\n  \"resourceTemplates\": [\n    {\n      \"uriTemplate\": \"file:///{path}\",\n      \"name\": \"Arbitrary local file access\",\n      \"description\": \"Allows reading any file by specifying {path}\"\n    }\n  ]\n}\n```\n\nThe client can then issue a `resources/read` request by providing the `path`:\n```json\n{\n  \"method\": \"resources/read\",\n  \"params\": {\n    \"uri\": \"file:///cpp/my_program.cpp\"\n  }\n}\n```\n\nThis will read from `/app/resources/cpp/my_program.cpp` and return the result:\n```json\n{\n  \"contents\": [\n    {\n      \"uri\": \"file:///cpp/my_program.cpp\",\n      \"mimeType\": \"text/plain\",\n      \"text\": \"int main(){}\"\n    }\n  ]\n}\n```\n\nNote the incorrect `text/plain` here - proper MIME detection will be added at some point.\n\nSimilarly to `DiscreteFileProvider`, when modifying a resource, call\n```kotlin\ntemplateFileProvider.onResourceChange(\"cpp/my_program.h\")\n```\nto trigger the notification in case a client is subscribed to this resource.\n\n**Use those FileProviders only in a sand-boxed environment, they are NOT production-ready.**\n\n---\n\n## Sampling\n\nClients can fulfill server-initiated LLM requests by providing a `SamplingProvider`.\n\nIn a real application, you would call your favorite LLM API (e.g., OpenAI, Anthropic) inside the provider. Here’s a simplified example that always returns a dummy completion:\n\n```kotlin\n// 1) Define a sampling provider\nval samplingProvider = SamplingProvider { params: CreateMessageParams -\u003e\n  CreateMessageResult(\n    model = \"dummy-model\",\n    role = Role.ASSISTANT,\n    content = TextContent(\"Dummy completion result\"),\n    stopReason = \"endTurn\",\n  )\n}\n\n// 2) Build the client with sampling support\nval client = Client.Builder()\n  .withTransport(StdioTransport())\n  .withPermissionCallback { userApprovable -\u003e \n    // Prompt the user for confirmation here\n    true \n  }\n  .withSamplingProvider(samplingProvider) // Register the provider\n  .build()\n\nrunBlocking {\n  client.start()\n  client.initialize()\n\n  // Now, if a server sends a \"sampling/createMessage\" request, \n  // the samplingProvider will be invoked to generate a response.\n}\n```\n\n---\n\n## Request Cancellations\n\nmcp4k uses Kotlin coroutines for cooperative cancellation. For example, a long-running server tool:\n\n```kotlin\n@McpTool\nsuspend fun slowToolOperation(iterations: Int = 10): ToolContent {\n  for (i in 1..iterations) {\n    delay(1000)\n  }\n  return \"Operation completed after $iterations\".toTextContent()\n}\n```\n\nThe client can cancel mid-operation:\n\n```kotlin\nval requestJob = launch {\n  client.sendRequest { id -\u003e\n    CallToolRequest(\n      id = id,\n      params = CallToolRequest.CallToolParams(\n        name = \"slowToolOperation\",\n        arguments = mapOf(\"iterations\" to 20),\n      ),\n    )\n  }\n}\ndelay(600)\nrequestJob.cancel(\"User doesn't want to wait anymore\")\n```\n\nUnder the hood, mcp4k sends a notification to the server:\n```json\n{\n  \"method\": \"notifications/cancelled\",\n  \"jsonrpc\": \"2.0\",\n  \"params\": {\n    \"requestId\": \"2\",\n    \"reason\": \"Client doesn't want to wait anymore\"\n  }\n}\n```\nand the server will abort the suspended tool operation.\n\n---\n\n## Roadmap\n\n```\n✅ Add resource capability\n✅ @McpTool and @McpPrompt functions\n✅ Request cancellations\n✅ Pagination\n✅ Sampling (client-side)\n✅ Roots\n✅ Transport logging\n⬜ Completions\n⬜ Support logging levels\n⬜ Proper version negotiation\n⬜ Emit progress notifications from @McpTool functions\n⬜ Proper MIME detection\n⬜ Add FileWatcher to automate resources/updated notifications\n⬜ HTTP-SSE transport\n⬜ Add references, property descriptions and validation keywords to the JSON schemas\n```\n\n---\n\n## How mcp4k Works\n\n- Annotated ```@McpTool``` and ```@McpPrompt``` functions are processed at compile time.\n- mcp4k generates JSON schemas, request handlers, and registration code automatically.\n- Generated code is injected during Kotlin's IR compilation phase, guaranteeing type-safe usage.\n- If your KDoc references unknown parameters, the build fails, forcing you to keep docs in sync with code.\n\n---\n\n## Contributing\n\nIssues and pull requests are welcome!\nFeel free to open a discussion or contribute improvements.\n\n**License**: mcp4k is available under the [Apache License 2.0](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fondrsh%2Fmcp4k","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fondrsh%2Fmcp4k","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fondrsh%2Fmcp4k/lists"}