Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:49:50 +08:00
commit adc4b2be25
147 changed files with 24716 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"name": "mxcp-plugin",
"description": "A Claude plugin for MXCP",
"version": "1.0.0",
"author": {
"name": "RAW Labs"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# mxcp-plugin
A Claude plugin for MXCP

617
plugin.lock.json Normal file
View File

@@ -0,0 +1,617 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:raw-labs/claude-code-marketplace:.claude-plugin/plugins/mxcp-plugin",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "4e714d6c5864c34727a2dfacb3ba9818c85bb10d",
"treeHash": "e065970c2bf43e0c06482499a934af49e7573360266bb2341993ae9154ffbbc1",
"generatedAt": "2025-11-28T10:27:48.113763Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "mxcp-plugin",
"description": "A Claude plugin for MXCP",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "a009df5cc5e005d8320dbb28240eff156d2b7dc6fb2e2f4ae0687a96fc18a663"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "9464fcb9cf8ea4e7fd4652bc8f80b491785ed6d8f94f1e1b3c98ac9d91044a87"
},
{
"path": "skills/mxcp-expert/llms.txt",
"sha256": "567959f05e313b59f4700e50ad1a5ac16eb56850da90591dae0de60230ab7619"
},
{
"path": "skills/mxcp-expert/README.md",
"sha256": "056135b4d7f9b96f04b089bf6869f444ed7c69dae9881f37c483db10875e77d2"
},
{
"path": "skills/mxcp-expert/SKILL.md",
"sha256": "16b425bf47ff1e8a498674fa5e6fcdd35dcd13b9c946e7290c93b61d2c2ffb66"
},
{
"path": "skills/mxcp-expert/references/synthetic-data-patterns.md",
"sha256": "3be31ac12c6866994ff564b7fb85241ba39b491c99e6eb663c622f791620666d"
},
{
"path": "skills/mxcp-expert/references/duckdb-essentials.md",
"sha256": "4668426477b7ab3acfc0e79e90991168c5fc22ee1d09a6cc2ddd337780922e31"
},
{
"path": "skills/mxcp-expert/references/type-system.md",
"sha256": "ad55678ed61e26bd116c3f67b37c73e7b62cadfd4d2d1682409eddfa93f10628"
},
{
"path": "skills/mxcp-expert/references/agent-centric-design.md",
"sha256": "a9bff3f01b1be49fd9f00e410ae05122878ec8b20a1633b1c50598f646401885"
},
{
"path": "skills/mxcp-expert/references/mxcp-evaluation-guide.md",
"sha256": "227ce5d3aeff3dfab46b60b682118d2b4aae9be4b992db03a4c35c1c7e658ed3"
},
{
"path": "skills/mxcp-expert/references/dbt-core-guide.md",
"sha256": "299d2aa1adb6e54acfead9d4f3986f014f3d1fed580602dc8adfa10d3a59ccb0"
},
{
"path": "skills/mxcp-expert/references/project-selection-guide.md",
"sha256": "4526f3a2e3ee45c877cdb14125223764be3111c24c585e92f0994904b2c62a3f"
},
{
"path": "skills/mxcp-expert/references/policies.md",
"sha256": "b0b73bee534f9eeb9f620880a472a6700461f34e9b45ca6bdbfd168864c56156"
},
{
"path": "skills/mxcp-expert/references/tool-templates.md",
"sha256": "a5eca354ae91ae8009493a855332d8320728bfd655280b1159c1327cd7383c47"
},
{
"path": "skills/mxcp-expert/references/endpoint-patterns.md",
"sha256": "7fcae5c115859223b8e4805a54806b440c949ed36d355b11ddceef052411a031"
},
{
"path": "skills/mxcp-expert/references/excel-integration.md",
"sha256": "82bf62a47d44273f09f47e43092e94b68be8cb2e1172b069022149e4418545b6"
},
{
"path": "skills/mxcp-expert/references/minimal-working-examples.md",
"sha256": "053348b2f06b7465ae7f4b2ccb63433e2cc272caa18ab1f63377303bccf2537b"
},
{
"path": "skills/mxcp-expert/references/python-development-workflow.md",
"sha256": "9a74637fd5fac5a814c0cd11b61f52f86f23cc91a6c645f0b7af48b631058cb6"
},
{
"path": "skills/mxcp-expert/references/comprehensive-testing-guide.md",
"sha256": "a7381352b2a5d3da38136a9797d3eaf1b6536f57436183eb928850357ac9cee9"
},
{
"path": "skills/mxcp-expert/references/python-api.md",
"sha256": "b34526e796484cf20fc0b5b5487ff9657e185359b4b59f6698446063c5f7e3aa"
},
{
"path": "skills/mxcp-expert/references/llm-friendly-documentation.md",
"sha256": "a03c0a2b931f9eea4345f2a17e007a74a7764b73ebcf452701da5d9cb92e715e"
},
{
"path": "skills/mxcp-expert/references/debugging-guide.md",
"sha256": "8af3585610e78afb6638790ba1f9dd814d3d0a2d2e3bc17020ef3b78d610ca56"
},
{
"path": "skills/mxcp-expert/references/dbt-patterns.md",
"sha256": "7d97306574581735c66d3dcdb7b6fa67330b616f165faaf047aaab2ec29db1b0"
},
{
"path": "skills/mxcp-expert/references/cli-reference.md",
"sha256": "26ee767dca68b501b464eef3a4ae88dca74d98f580cbd232ce32ea9d98300f1b"
},
{
"path": "skills/mxcp-expert/references/claude-desktop.md",
"sha256": "45a182ba75239a991a4b6a4fdca8e56a581500772fb2d275cccccbdf0981959f"
},
{
"path": "skills/mxcp-expert/references/database-connections.md",
"sha256": "77337236b0503ee1a4ccc3bdcb1cf4578beea86d3d98d9abe901baf476aacc93"
},
{
"path": "skills/mxcp-expert/references/testing-guide.md",
"sha256": "513913db746593825d8a33631190a7de0f13ab164c2aec1a3d8649762c50fefb"
},
{
"path": "skills/mxcp-expert/references/build-and-validate-workflow.md",
"sha256": "39929c16fa1918da5602233095211e41a3430ad0958c85c297492a4007fe17cc"
},
{
"path": "skills/mxcp-expert/references/error-handling-guide.md",
"sha256": "21dd48c7fbfcaf8238b9a97bdf04211af5c21ff8a714eb6583aab44bfbf42985"
},
{
"path": "skills/mxcp-expert/scripts/validate_yaml.py",
"sha256": "3a907ef722690a3d763b9b7edd223a275eb055b889b86021da32938ca955bb69"
},
{
"path": "skills/mxcp-expert/assets/schemas/mxcp-config-schema-1.json",
"sha256": "6e1eefed8ae59f3875a614b2f09c52d968cbe1bd0409e33fce2079c980ed5972"
},
{
"path": "skills/mxcp-expert/assets/schemas/mxcp-site-schema-1.json",
"sha256": "7f0e0ab37d48c733a4daf6bc28ddf76254be35864a0dcfc472caa781dc78720f"
},
{
"path": "skills/mxcp-expert/assets/schemas/drift-snapshot-schema-1.json",
"sha256": "5b10c612221cf9aa01e9c621883510d4e89140d4355ae08e272044a724c12ff8"
},
{
"path": "skills/mxcp-expert/assets/schemas/common-types-schema-1.json",
"sha256": "509f4690bbe850c1acc01923db1f5d0f56cc8cab1081e9dd0dd57ddebc16432c"
},
{
"path": "skills/mxcp-expert/assets/schemas/eval-schema-1.json",
"sha256": "f471f062733b30b89d223a1afe2a1d901053dd9a2a3e7a2a65be9b12040db69b"
},
{
"path": "skills/mxcp-expert/assets/schemas/tool-schema-1.json",
"sha256": "7a5bac4698630e3553152097ddc8e43f163f44b460e000c612b77fc96fe1739b"
},
{
"path": "skills/mxcp-expert/assets/schemas/prompt-schema-1.json",
"sha256": "1aa8bcb83d7c444aa5f9ab7b691f185f7c9aa0a3558531209fd9e6ba75eafff8"
},
{
"path": "skills/mxcp-expert/assets/schemas/resource-schema-1.json",
"sha256": "850d00aa834dd272b0559d75db5a349262323793a4e6486a69990d6d32dd9865"
},
{
"path": "skills/mxcp-expert/assets/schemas/drift-report-schema-1.json",
"sha256": "4d6bc5343e3ee5d87f13265b4dea46ea0effe43c6a558b57437ca1c0ab1804e7"
},
{
"path": "skills/mxcp-expert/assets/project-templates/plugin/README.md",
"sha256": "9e6fa239223787a0e74b972bdcef3405895ee5fd13422e94819b0b91a121248b"
},
{
"path": "skills/mxcp-expert/assets/project-templates/plugin/config.yml",
"sha256": "4433b883f0ecb1bac345910e5aaa4efaf19ec9668f7519f1beeb9006a402eb4e"
},
{
"path": "skills/mxcp-expert/assets/project-templates/plugin/mxcp-site.yml",
"sha256": "863657c532fd430e68e0e4c15523a33ced193306470f5896e93457d7ed1458ed"
},
{
"path": "skills/mxcp-expert/assets/project-templates/plugin/tools/decipher.yml",
"sha256": "ee2420c5a8acdb91c9d1b69ff20a9a896cbe347111189b0ca198e0c16acf4d8e"
},
{
"path": "skills/mxcp-expert/assets/project-templates/plugin/plugins/README.md",
"sha256": "a19de0cd5c0edfa98028d2e64b880278fc44cdd615f451b34cd359ede44ee92e"
},
{
"path": "skills/mxcp-expert/assets/project-templates/plugin/plugins/my_plugin/__init__.py",
"sha256": "4704bd6e097614851f13955d056479789f2126b6fb729cf148a3fcbc514c6190"
},
{
"path": "skills/mxcp-expert/assets/project-templates/python-demo/README.md",
"sha256": "6ff4ef172b2d86a914d98347ad90dbb80403f4225b5f020cc6265658c530d181"
},
{
"path": "skills/mxcp-expert/assets/project-templates/python-demo/mxcp-site.yml",
"sha256": "8d3ad2691ce6fbfbc80dfbaa1c5a5406403b5ec547d2dab597a02159d068e0e5"
},
{
"path": "skills/mxcp-expert/assets/project-templates/python-demo/tools/aggregate_by_category.yml",
"sha256": "4fcf90e1f252adb709fc18db31e60156bc60757c4ca3d67100ec4e5f6c5de4f0"
},
{
"path": "skills/mxcp-expert/assets/project-templates/python-demo/tools/analyze_numbers.yml",
"sha256": "238183dfc8c3456c68db2dec09a01bbf17732c8022515751903feba7c6210423"
},
{
"path": "skills/mxcp-expert/assets/project-templates/python-demo/tools/process_time_series.yml",
"sha256": "6bbfff9d53df54a8ba1ac30a4cfdb05804b48af9569e73f79750a8b8ce005ec2"
},
{
"path": "skills/mxcp-expert/assets/project-templates/python-demo/tools/create_sample_data.yml",
"sha256": "58a7c46a1e84756f982603b504e5bf8a719a21364e5376cd95df6086857f131b"
},
{
"path": "skills/mxcp-expert/assets/project-templates/python-demo/python/data_analysis.py",
"sha256": "ecef2a18ab2e8b0cbd44ac4743370bce6d2b5caf4ee0c14e2e3bb5e6804af3b7"
},
{
"path": "skills/mxcp-expert/assets/project-templates/python-demo/python/primitive_arrays.py",
"sha256": "48cf51fee0002a53530e117852567bf0fedc192b4d69b4313582b9b51f1a6e85"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/README.md",
"sha256": "e32069faec8556945e8a4c03b864e3d26efa4e0f1861926a30082bb93b682855"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/config.yml",
"sha256": "2c6923c365a7ec99f7562f18454f72d43793e7012195b2aefb2bc0499550e5a3"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/mxcp-site.yml",
"sha256": "0165bc3fb3f3dbde21fadfd89c00a3c5d8ca37e959d4578694cb1d8f86a083e7"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/tools/search_events.yml",
"sha256": "6eebeb69717951c7c6544eb72ed6e799aabe0ddd8681d28f45f4f28fa82f6b9f"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/tools/get_event.yml",
"sha256": "ba61b7d35bb42ebf8dafd2d5b9560f05f8519fa2b18bda8fe159976c2dbc2b3f"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/tools/get_freebusy.yml",
"sha256": "88297a6edddd48b6c30a56ef7ec3b33f5ff0e8e1b021584a83bb2833da0e703d"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/tools/get_calendar.yml",
"sha256": "e762d81fb32d5bc925edb92ba0e66c1e4826218409e8e30f5bf1eec994257fa4"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/tools/list_events.yml",
"sha256": "05665c19097cae1be17fe080b54a360baccd40e6bacbabfd768a094c82a5541e"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/tools/list_calendars.yml",
"sha256": "02342c3e6547c1fc6d536f13ae46560400b78c44d01c84c4c16999973cc98258"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/tools/whoami.yml",
"sha256": "b80be91b28834c644144c9de879a2dd99c6130a58214d83bb1de8d9040eadaa2"
},
{
"path": "skills/mxcp-expert/assets/project-templates/google-calendar/python/google_calendar_client.py",
"sha256": "6b769d9330bda66fc8d8345c4514080bdadc40b8002e8e309efd5391febc49c1"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/README.md",
"sha256": "859f2a68e08327be3203be0d2724d15ddacb65228703e630981104b6d22d459d"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/config.yml",
"sha256": "b0ca8db576be659ce5a9a945e405a4040c7467de7cd03d636ccd88a65433f12d"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/mxcp-site.yml",
"sha256": "4509f3eed2d11a8730e95ed10867800fd8d04906d4ebc1a3f4d60d69a00424a6"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/tools/jql_query.yml",
"sha256": "f823682586b1236e7730bbef09c457a3c0bdcbaf1ff416889e1fb6614d4d737b"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/tools/get_issue.yml",
"sha256": "4477022fbda39c60db9fa8983e86e5aa925fd0ed9574c230a7c9122390b5746c"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/tools/get_user.yml",
"sha256": "c5144bc7acd0a85b988c561db0d8372df53c907cfe66b5baf5ed8fae639accd2"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/tools/get_project_role_users.yml",
"sha256": "d3585503bfb4c653280a75d622a7f8d51faeef1f90e35e465f42e2a385df00b1"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/tools/get_project_roles.yml",
"sha256": "72b7390f9564b60ff1d3eb4859dee9e693269fb6695729b6f9aa21a60bf219b5"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/tools/list_projects.yml",
"sha256": "e3c1d60b14337edd945f4a125ca8e3300f4190d0a3e92518dccbcce6f7a2c52c"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/tools/get_project.yml",
"sha256": "90f9006b0bc64206032b949f79507e7a0312aa3cf08ab26b4294e9758eedc318"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/tools/search_user.yml",
"sha256": "4511bea30fda3dea501a405ad7ba23d0d38628152152509ce7011d6b2dd6676a"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira/python/jira_endpoints.py",
"sha256": "dc8481f0a4a033bd1c81cf8ece0036157aef73aad7506eabf4c71390e2a0ec57"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/README.md",
"sha256": "79b9d29a1dddfcfe05411637f3dfa72f5eac35ab25aab9bbbc037d41aa6fc18b"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/config.yml",
"sha256": "cd0b5db0dae014a69670dc8366df354624fd51fb04cf3009d0018e09fa0a01f0"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/mxcp-site.yml",
"sha256": "d0c6a512ee85f5a58f8fd6b727e5014ec172f15245888a9f0def5dda4898bbc9"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/tools/cql_query.yml",
"sha256": "f011f4fef76e95af6d5500261b920acd5c868087fb1a467895511f5b9d6da7cc"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/tools/get_page.yml",
"sha256": "a236ae6c965769bdc5860f7cceb0b17cf268861ba675009546dbe32e70c59685"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/tools/get_children.yml",
"sha256": "f23c90c6e568675d766ad845f48bac58760cd0115f1e59db2a94cbb435b4d3f2"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/tools/describe_page.yml",
"sha256": "3eb376b26b85a7443012a7c4fc3fa6c0bdc124bbaffd7a21aeaca20dd7cfb622"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/tools/list_spaces.yml",
"sha256": "f93d168b96d5fcc6d031d244ecbe11c487c53be466146dfbaa09c672aa5e137a"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/tools/search_pages.yml",
"sha256": "fd05e6f5ded86cfef7ff855d773bd4126c6bb9e43ca09737cd761d5667ee423b"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/plugins/mxcp_plugin_confluence/__init__.py",
"sha256": "d5e66b8de3a915f485bfdcf2d4832c62da680a95c6c601ec2c2f073e9f7a08a6"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/sql/get_children.sql",
"sha256": "08167e0b44e3aca9f054ebe3fcf402f5d8b18da5dd7e37e38dc67ab4b3caedbf"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/sql/cql_query.sql",
"sha256": "b159a0b01fb2dcb16dd028cdcd05bc245a1e6f07dfc627ceb649f6c456e4e14f"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/sql/get_page.sql",
"sha256": "25f441196bde824db1321facd71e93323c8c5903766bf9fae0e46f58e57ef219"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/sql/search_pages.sql",
"sha256": "fb443bed4a9fd834a53b09edf80602711d6ab6035859fa0c48e83d321dfb8361"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/sql/list_spaces.sql",
"sha256": "f465cdca33663cca5ca2e5679978a3669d0b88578f885bf9c433d9f4baddae93"
},
{
"path": "skills/mxcp-expert/assets/project-templates/confluence/sql/describe_page.sql",
"sha256": "d56d22ab4c2e68bcb1be97b4c83a1e153e7461f1f9819170e83c825ffe229182"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/README.md",
"sha256": "2d3d1bfd9dc2a72d7735af9fdbe5b5ba835c9caeb7902208de7506c97da10d68"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/config.yml",
"sha256": "93cc041c548fa586081cc7210a609c3797803578f964f7ce9c24853964bbf843"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/mxcp-site.yml",
"sha256": "da34832c3e57b4d1c7594c141e09162e1c31f7a83f80cdd2b8e657207b1f7f09"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/tools/get_sobject.yml",
"sha256": "1619e8912ec931607584804f03d571e2fef26b113b2a2453076d0f3aa58df4ce"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/tools/describe_sobject.yml",
"sha256": "7afc1e46b7b17887f2cbaaa24ea4d318e15dd64f60f9d8f1c139da9b36c797bd"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/tools/soql.yml",
"sha256": "44adadf6590cc69e326ac8bfb3b58225a27914156ad2060afc7ce1400526b4f4"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/tools/whoami.yml",
"sha256": "c649d8402c117da349b005bc8e8cab4911f7b63f1f42fb8c7b7d4bfdf68fdb15"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/tools/list_sobjects.yml",
"sha256": "beb7d299358e81efec2079a9840797b995296e1a60d1dcfd7016ce4753810e0d"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/tools/sosl.yml",
"sha256": "fe53d81b7664ccb8adf2df62077eb8055e0d9a2ec9c717260b2ff8c59d733974"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/tools/search.yml",
"sha256": "8d973d8c874f5779e40e4e2916ee7504c1910d67631da40179c030c4732a436e"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce-oauth/python/salesforce_client.py",
"sha256": "cb6d4747bcc350cc723dc66173191017ea8dfe9c9ecd109274a33a3309a491fc"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/README.md",
"sha256": "6a54669282e11989e60a09eae615f617663c9fc03aa88d940d490c303432c448"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/.gitignore",
"sha256": "d63240c974b32776149b49f1de2ef051213846340c5f769de85f0ef3c51bb732"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/mxcp-site.yml",
"sha256": "7fd85d2a799e1c22310dc4c1df39edec03e35e4f13ceadfd3c5fb7ffe753da81"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/dbt_project.yml",
"sha256": "2d50823c6160bd6ed4996bb575665fd9f018fc5eeeef1f7f3f2348012d051e44"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/models/covid_data.sql",
"sha256": "dfb7ab7a8713985315349e9a9202c5a7afb464994da416159242cfffae5458b5"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/models/hospitalizations.sql",
"sha256": "9379b1439f4daae9c446a9903e09efa81eb3de83389bb386a58181c35dfdf635"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/models/locations.sql",
"sha256": "eab3e7b87c2d0181ad28bad6ecbe6e611a8bcd90ade83fe1b72eb465efbd70a6"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/models/sources.yml",
"sha256": "0f78b23f49253d964a7df3408a668ff903e51503b576f1f11bbdeaf778020349"
},
{
"path": "skills/mxcp-expert/assets/project-templates/covid_owid/prompts/prompt.yml",
"sha256": "6456683c982fba7b00b3ad001757c6f969898fbfa56d1243c67bb7f9765bcb40"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/README.md",
"sha256": "53dccf841faf792eafe4389edc69ac36b1f617eee06ec15ea3e45bd40feacfbf"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/config.yml",
"sha256": "b3091c55f34129f7538f3810177edd01b5b18ed6e1240bc9e56f58a6d691977c"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/mxcp-site.yml",
"sha256": "5f398c932e941530c75c092ce652a94a52a40d44de081b5117f268c41d9e6fa7"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/tools/get_sobject.yml",
"sha256": "bb5c86ff480bcb6a949934d209c5ecaa2e3f7dd475725be680c36e280fc00257"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/tools/describe_sobject.yml",
"sha256": "6cb4ba330b019ff23576678c5c65faebe1cf17b65fbe645e338ea14a0d5527a4"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/tools/soql.yml",
"sha256": "7813ceaeed5568c1bc360fa0e217eb5f02444a33f15d61df077a7eef1bc032e0"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/tools/list_sobjects.yml",
"sha256": "209cd5654e2048e7e12f6fcc2face42ba4e62e677225daa8de582287a5838538"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/tools/sosl.yml",
"sha256": "5df116c377e203266ddc698be7c6b86a1545646fe78a744b96b52c58afdf4c58"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/tools/search.yml",
"sha256": "9b34ce01c2c0ed32ab4f7265aba143bb758c3caabaa75371650d36c7faacadef"
},
{
"path": "skills/mxcp-expert/assets/project-templates/salesforce/python/salesforce_endpoints.py",
"sha256": "e64d385d2a69d2141e2208da3576d8456b356146f869cf0172b06efcf9eee790"
},
{
"path": "skills/mxcp-expert/assets/project-templates/keycloak/README.md",
"sha256": "a864e7e52afa34ba462ce2301b8cc30a6be3fc930402fe10cb2687f5608cd25b"
},
{
"path": "skills/mxcp-expert/assets/project-templates/keycloak/config.yml",
"sha256": "71bab5920e47c7aefad19dcff5496514bd5a7b46906add982cf433070fe03353"
},
{
"path": "skills/mxcp-expert/assets/project-templates/keycloak/mxcp-site.yml",
"sha256": "b230eb5349590406c57d11955733984d19953d81d10f7c072684caa021acf04f"
},
{
"path": "skills/mxcp-expert/assets/project-templates/keycloak/tools/get_user_info.yml",
"sha256": "ccfab0d10c15e7e2e665f7daa3f2a495df007e434cc1fe284b2710623489818f"
},
{
"path": "skills/mxcp-expert/assets/project-templates/squirro/data/db-default.duckdb",
"sha256": "a71a5d75c93d9308cfb1210f03b268b38bef17dea8aaf83224b034a4e68e5567"
},
{
"path": "skills/mxcp-expert/assets/project-templates/earthquakes/README.md",
"sha256": "6d44a0948a4cfb4275241424e5b8068be6fde0e4bfb0911cd0545c3cec2cc8f8"
},
{
"path": "skills/mxcp-expert/assets/project-templates/earthquakes/mxcp-site.yml",
"sha256": "6f03282179c556e6a379b57e586a9d7469ce5a78eff631170887cf54167d07e6"
},
{
"path": "skills/mxcp-expert/assets/project-templates/earthquakes/tools/tool.yml",
"sha256": "116a967018aeeda99d78a69179e517bd0a6083eec2cd84fb84531bdd22f7387d"
},
{
"path": "skills/mxcp-expert/assets/project-templates/earthquakes/prompts/prompt.yml",
"sha256": "ca9ab14b805132adad341a084303365f17e9273ec38282fc7fa6cc2d4812aa2a"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/README.md",
"sha256": "da4f8ba9bf73932da57283bf231283c147771b290ff8794d59f3e111039a2013"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/config.yml",
"sha256": "0cc077f3b1e961d04539a7d8c6dd08805a41db72278d17f76f128cb352a06f56"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/mxcp-site.yml",
"sha256": "b4577dae8ce6eb230e4be6e92c0ca5803d0a2b399b693755e6370ec3ab6d8ee3"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/tools/get_current_user.yml",
"sha256": "130ddc0ac8bbbd58d326abedf1d8271f9cc4178c11a5adbf7644fb314b1b9080"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/tools/get_user.yml",
"sha256": "3a19ec60b803d62d1a2750cdc829d27bada5cdc227af9719e58176f1483d732e"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/tools/jql.yml",
"sha256": "ade223047da84d046d5255561a8c646f2f92aa7a8dbc35540fa2b0c6283ed959"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/tools/list_projects.yml",
"sha256": "ee8a9ffc1afeaed6d07b0db52df79f9fd4972bccd6795a9978cb013d341f7180"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/tools/get_project.yml",
"sha256": "89f507bbfe477a568c7104d66eeea7f9006d27ac88cb84dd74c72adf6257f362"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/plugins/mxcp_plugin_jira_oauth/__init__.py",
"sha256": "a39efdcabc2a5ead7ee04d2db68a3c482d44e88808ae4e9696ab196a1df30392"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/plugins/mxcp_plugin_jira_oauth/plugin.py",
"sha256": "5251d7d2244f2f2fd9a7494be94c9e1de8146831adacb9269383107c193e06bd"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/sql/get_current_user.sql",
"sha256": "d6960073044cdfde6a0a405bd34174bfaa5230960fd4a2bcb66b107679e375c0"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/sql/get_project.sql",
"sha256": "f0c5dfda853632aad7e8fafcefafd739219187e08fcb70e2103dac6d217485ae"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/sql/list_projects.sql",
"sha256": "dad64ae1a41283661648c3e7a9c2ea6dec81fabc3266b9bf4707d71785ca3810"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/sql/get_user.sql",
"sha256": "7f8121d434d948c3840edaba1e9a657c32e6ba755181bf32c43d6f996b1fe750"
},
{
"path": "skills/mxcp-expert/assets/project-templates/jira-oauth/sql/jql.sql",
"sha256": "748503e32c0a115ba22affc62c6d6592042fdb827973582bce238bbe37f3e96e"
}
],
"dirSha256": "e065970c2bf43e0c06482499a934af49e7573360266bb2341993ae9154ffbbc1"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,273 @@
# MXCP Expert Claude Skill
A comprehensive Claude skill for working with MXCP (Enterprise MCP Framework) - enabling you to build production-ready MCP servers with SQL and Python endpoints, complete with security, audit trails, and policy enforcement.
## What's Included
This skill provides complete guidance for:
- **Creating MXCP projects** - Initialize and structure production-ready MCP servers
- **Endpoint development** - Build tools, resources, and prompts using SQL or Python
- **Enterprise features** - Implement authentication, policies, and audit logging
- **dbt integration** - Combine data transformation with MCP endpoints
- **Quality assurance** - Validate, test, lint, and evaluate your endpoints
- **Production deployment** - Monitor drift, track operations, and ensure security
## Installation
1. Download the `mxcp-expert.zip` file
2. In Claude Desktop, go to Settings → Developer → Custom Skills
3. Click "Add Skill" and upload the `mxcp-expert.zip` file
4. The skill will be automatically available when working on MXCP projects
## Skill Structure
```
mxcp-expert/
├── SKILL.md # Main skill file with quick reference
├── assets/ # Project templates and resources
│ ├── project-templates/ # Pre-built MXCP projects
│ │ ├── google-calendar/ # OAuth integration examples
│ │ ├── jira/ jira-oauth/ # Jira integrations
│ │ ├── salesforce/ salesforce-oauth/ # Salesforce integrations
│ │ ├── confluence/ # Confluence integration
│ │ ├── python-demo/ # Python endpoint patterns
│ │ ├── covid_owid/ # dbt data caching example
│ │ ├── keycloak/ # SSO integration
│ │ └── ... # More templates
│ └── schemas/ # JSON Schema definitions for YAML validation
│ ├── mxcp-site-schema-1.json # mxcp-site.yml validation
│ ├── mxcp-config-schema-1.json # config.yml validation
│ ├── tool-schema-1.json # Tool definition validation
│ ├── resource-schema-1.json # Resource definition validation
│ ├── prompt-schema-1.json # Prompt definition validation
│ ├── eval-schema-1.json # Evaluation suite validation
│ └── common-types-schema-1.json # Common type definitions
├── scripts/ # Utility scripts
│ └── validate_yaml.py # YAML validation script
└── references/ # Detailed documentation (23 files)
├── tool-templates.md # Ready-to-use YAML templates for tools/resources/prompts
├── project-selection-guide.md # Decision tree and template selection
├── dbt-core-guide.md # Essential dbt knowledge (seeds, models, Python models)
├── duckdb-essentials.md # DuckDB features and SQL extensions
├── endpoint-patterns.md # Complete endpoint examples
├── python-api.md # Python runtime API reference
├── policies.md # Policy enforcement guide
├── comprehensive-testing-guide.md # Complete testing strategies
├── debugging-guide.md # Systematic debugging workflows
├── error-handling-guide.md # Python error handling patterns
├── dbt-patterns.md # Advanced dbt integration patterns
├── database-connections.md # PostgreSQL, MySQL, SQLite, SQL Server
├── excel-integration.md # Excel file handling with pandas and dbt
├── claude-desktop.md # Claude Desktop setup guide
├── cli-reference.md # Complete CLI documentation
├── type-system.md # Type validation reference
└── ... # 8 more specialized guides
```
## When This Skill is Used
Claude will automatically use this skill for ANY mention or reference to MXCP, including when you:
- Mention "MXCP", "mxcp", "MXCP server", "MXCP project" in any context
- Ask questions about MXCP or its features
- Create, initialize, build, or set up MXCP projects
- Edit or modify MXCP files (mxcp-site.yml, config.yml, tools/resources/prompts)
- Define or work with MCP tools, resources, or prompts using MXCP
- Implement SQL queries or Python endpoints for MXCP
- Configure MXCP features (authentication, policies, audit logging, dbt integration)
- Run MXCP commands (mxcp validate, mxcp test, mxcp serve, mxcp init, mxcp lint)
- Test, validate, lint, or debug MXCP endpoints
- Fix MXCP validation errors or troubleshoot issues
- Deploy MXCP servers to production
## Quick Start Examples
### Example 1: CSV File to MCP Server
```
User: "I need to connect my chat to a CSV file with customer data"
Claude: [Uses mxcp-expert skill]
I'll help you create an MXCP server that queries your CSV file.
1. Initialize project:
mkdir customer-mcp && cd customer-mcp
mxcp init --bootstrap
2. Add your CSV as a dbt seed:
cp customers.csv seeds/
3. Create schema definition (seeds/schema.yml):
[Creates proper schema.yml with column types and tests]
4. Load data:
dbt seed
dbt test
5. Create query tool:
[Creates tools/get_customers.yml with parameterized SQL]
6. Test and validate:
mxcp validate && mxcp test
mxcp serve
[Provides complete security-first implementation with validation]
```
### Example 2: API Integration
```
User: "Connect to Google Calendar API"
Claude: [Uses mxcp-expert skill]
I'll use the google-calendar project template for OAuth integration.
1. Copy template:
cp -r assets/project-templates/google-calendar calendar-mcp
cd calendar-mcp
2. Configure OAuth in project config.yml:
[Creates config.yml with OAuth settings in project directory]
3. Test authentication:
mxcp serve
[Explains OAuth flow]
[Provides complete working OAuth integration]
```
### Example 3: Database Connection
```
User: "Connect to my PostgreSQL production database"
Claude: [Uses mxcp-expert skill]
I'll help you connect to your PostgreSQL database using DuckDB ATTACH.
1. Create project and config:
mkdir postgres-mcp && cd postgres-mcp
mxcp init --bootstrap
Create config.yml with database credentials (uses environment variables)
2. Create SQL tool with ATTACH:
[Creates tools/query_customers.yml with PostgreSQL ATTACH statement]
3. Set credentials and test:
export DB_HOST="localhost" DB_USER="readonly_user" DB_PASSWORD="xxx"
mxcp validate && mxcp run tool query_customers
4. Alternative: Cache data with dbt for fast queries
[Shows dbt source + model pattern to materialize data]
[Provides both direct query and cached approaches with security best practices]
```
## Key Features Covered
### Endpoint Development
- SQL tools for data queries
- Python tools for complex logic
- Resources for data access
- Prompts with Jinja templates
- Combined SQL + Python patterns
### Enterprise Features
- OAuth authentication (GitHub, Google, Microsoft, etc.)
- Policy-based access control with CEL expressions
- Comprehensive audit logging (JSONL format)
- Field-level data filtering and masking
- User context testing
### Quality Assurance
- Structure validation with `mxcp validate`
- Functional testing with `mxcp test`
- Metadata quality checks with `mxcp lint`
- LLM behavior testing with `mxcp evals`
### dbt Integration
- Data transformation pipelines
- External data caching
- Incremental model patterns
- Data quality tests
### Production Operations
- Drift detection and monitoring
- Audit log querying and export
- Multi-environment profiles
- Secrets management (Vault, 1Password)
- OpenTelemetry observability
## Core Principles
This skill prioritizes:
1. **Security First** - Authentication, authorization, parameterized queries, input validation
2. **Robustness** - Error handling, type safety, data quality checks
3. **Validity** - Schema compliance, structure validation
4. **Testability** - Comprehensive test coverage
5. **Testing** - Always validate/test/lint before deployment
## Mandatory Workflow
**To ensure MXCP servers always work correctly, the agent follows:**
1. **Build incrementally** - One tool/component at a time
2. **Validate continuously** - `mxcp validate` after each change
3. **Test before proceeding** - `mxcp test` must pass before next step
4. **Verify manually** - Run actual tool with real data
5. **Definition of Done** - ALL validation checks must pass
The agent will NEVER declare a project complete unless all validation, tests, and manual verification succeed.
## Documentation Coverage
The skill includes comprehensive documentation based on official MXCP docs:
**CRITICAL - Start Here**:
- **build-and-validate-workflow.md** - MANDATORY workflow ensuring correctness
- **minimal-working-examples.md** - Guaranteed-to-work examples (copy, test, customize)
**Essential Guides** (for most projects):
- **project-selection-guide.md** - Decision trees, heuristics, when to use which approach
- **dbt-core-guide.md** - dbt seeds, models, schema.yml (critical for CSV/data projects)
- **duckdb-essentials.md** - DuckDB SQL features, CSV import, analytics
- **excel-integration.md** - Excel file handling and pandas integration
- **synthetic-data-patterns.md** - Generate test data with GENERATE_SERIES
**Detailed References**:
- **endpoint-patterns.md** - Complete tool/resource/prompt examples
- **python-api.md** - Python runtime API and library wrapping patterns
- **testing-guide.md** - Comprehensive testing strategies
- **policies.md** - Policy enforcement and security
- **type-system.md** - Type validation rules
- **cli-reference.md** - Complete CLI documentation
- **claude-desktop.md** - Claude Desktop integration
- **dbt-patterns.md** - Advanced dbt integration patterns
## About MXCP
MXCP is an enterprise-grade MCP (Model Context Protocol) framework that provides a structured methodology for building production AI applications:
1. **Data Quality First** - Start with dbt models and data contracts
2. **Service Design** - Define types, security policies, and API contracts
3. **Smart Implementation** - Choose SQL for data, Python for logic
4. **Quality Assurance** - Validate, test, lint, and evaluate
5. **Production Operations** - Monitor drift, track audits, ensure performance
## License
This skill compiles information from the MXCP project documentation.
MXCP is released under the Business Source License 1.1 (BSL).
For more information about MXCP:
- Website: https://mxcp.dev
- GitHub: https://github.com/raw-labs/mxcp
- Contact: mxcp@raw-labs.com
## Skill Version
Version: 1.0.0
Created: October 2025
Based on: MXCP documentation as of October 2025

1059
skills/mxcp-expert/SKILL.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
# MXCP Confluence Plugin Example
This example demonstrates how to use MXCP with Confluence data. It shows how to:
- Create and use a custom MXCP plugin for Confluence integration
- Query Confluence content using SQL
- Combine Confluence data with other data sources
## Overview
The plugin provides several UDFs that allow you to:
- Search pages using keywords and CQL queries
- Fetch page content and metadata
- List child pages and spaces
- Navigate the Confluence content hierarchy
## Configuration
### 1. Creating an Atlassian API Token
**Important:** This plugin currently only supports API tokens **without scopes**. While Atlassian has introduced scoped API tokens, there are known compatibility issues when using scoped tokens with basic authentication that this plugin relies on.
To create an API token without scopes:
1. **Log in to your Atlassian account** at [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
2. **Verify your identity** (if prompted):
- Atlassian may ask you to verify your identity before creating API tokens
- Check your email for a one-time passcode and enter it when prompted
3. **Create the API token**:
- Click **"Create API token"** (not "Create API token with scopes")
- Enter a descriptive name for your token (e.g., "MXCP Confluence Integration")
- Select an expiration date (tokens can last from 1 day to 1 year)
- Click **"Create"**
4. **Copy and save your token**:
- Click **"Copy to clipboard"** to copy the token
- **Important:** Save this token securely (like in a password manager) as you won't be able to view it again
- This token will be used as your "password" in the configuration below
### 2. User Configuration
Add the following to your MXCP user config (`~/.mxcp/config.yml`). You can use the example `config.yml` in this directory as a template:
```yaml
mxcp: 1
projects:
confluence-demo:
profiles:
dev:
plugin:
config:
confluence:
url: "https://your-domain.atlassian.net/wiki"
username: "your-email@example.com"
password: "your-api-token" # Use the API token you created above
```
**Configuration Notes:**
- Replace `your-domain` with your actual Atlassian domain
- Replace `your-email@example.com` with the email address of your Atlassian account
- Replace `your-api-token` with the API token you created in step 1
- The `password` field should contain your API token, not your actual Atlassian password
### 2. Site Configuration
Create an `mxcp-site.yml` file:
```yaml
mxcp: 1
project: confluence-demo
profile: dev
plugin:
- name: confluence
module: mxcp_plugin_confluence
config: confluence
```
## Available Tools
### Search Pages
```sql
-- Search for pages containing specific text
SELECT search_pages_confluence($query, $limit) as result;
```
### Get Page
```sql
-- Fetch a page's content
SELECT get_page_confluence($page_id) as result;
```
### Get Children
```sql
-- List direct children of a page
SELECT get_children_confluence($page_id) as result;
```
### List Spaces
```sql
-- List all accessible spaces
SELECT list_spaces_confluence() as result;
```
### Describe Page
```sql
-- Show metadata about a page
SELECT describe_page_confluence($page_id) as result;
```
## Example Queries
1. Search and analyze page content:
```sql
WITH pages AS (
SELECT * FROM search_pages_confluence('important documentation', 50)
)
SELECT
p.title as page_title,
p.space.name as space_name,
p.version.number as version,
p.metadata.created as created_date
FROM pages p
ORDER BY p.metadata.created DESC;
```
## Plugin Development
The `mxcp_plugin_confluence` directory contains a complete MXCP plugin implementation that you can use as a reference for creating your own plugins. It demonstrates:
- Plugin class structure
- Type conversion
- UDF implementation
- Configuration handling
## Running the Example
1. Set the `MXCP_CONFIG` environment variable to point to your config file:
```bash
export MXCP_CONFIG=/path/to/examples/confluence/config.yml
```
2. Start the MXCP server:
```bash
mxcp serve
```
## Notes
- Make sure to keep your API token secure and never commit it to version control.
- The plugin requires proper authentication and API permissions to work with your Confluence instance.
- All functions return JSON strings containing the requested data.

View File

@@ -0,0 +1,12 @@
mxcp: 1
projects:
confluence-demo:
profiles:
dev:
plugin:
config:
confluence:
url: "https://your-domain.atlassian.net/wiki"
username: "your-email@example.com"
password: "your-api-token"

View File

@@ -0,0 +1,7 @@
mxcp: 1
project: confluence-demo
profile: dev
plugin:
- name: confluence
module: mxcp_plugin_confluence
config: confluence

View File

@@ -0,0 +1,172 @@
"""
Confluence Plugin Implementation
This module provides UDFs for interacting with Atlassian Confluence.
"""
import json
import logging
from typing import Any, Dict, List, Optional
from atlassian import Confluence
from mxcp.plugins import MXCPBasePlugin, udf
logger = logging.getLogger(__name__)
class MXCPPlugin(MXCPBasePlugin):
"""Confluence plugin that provides content query functionality."""
def __init__(self, config: Dict[str, Any]):
"""Initialize the Confluence plugin.
Args:
config: Plugin configuration containing Confluence API credentials
Required keys:
- url: The base URL of your Confluence instance
- username: Your Atlassian username/email
- password: Your Atlassian API token
"""
super().__init__(config)
self.url = config.get("url", "")
self.username = config.get("username", "")
self.password = config.get("password", "")
if not all([self.url, self.username, self.password]):
raise ValueError(
"Confluence plugin requires url, username, and password in configuration"
)
# Initialize Confluence client
self.confluence = Confluence(
url=self.url, username=self.username, password=self.password, cloud=True
)
@udf
def cql_query(
self, query: str, space_key: Optional[str] = None, max_results: Optional[int] = 50
) -> str:
"""Execute a CQL query against Confluence.
Args:
query: The CQL query string
space_key: Optional space key to limit the search
max_results: Maximum number of results to return (default: 50)
Returns:
JSON string containing matching pages
"""
logger.info(
"Executing CQL query: %s in space=%s with max_results=%s", query, space_key, max_results
)
# Build the CQL query
cql = query
if space_key:
cql = f'space = "{space_key}" AND {cql}'
# Execute the CQL query
results = self.confluence.cql(cql=cql, limit=max_results, expand="version,metadata.labels")
# Transform the response to match our schema
transformed_results = [
{
"id": page["content"]["id"],
"title": page["content"]["title"],
"space_key": page["content"]["space"]["key"],
"url": f"{self.url}/wiki/spaces/{page['content']['space']['key']}/pages/{page['content']['id']}",
"version": {
"number": page["content"]["version"]["number"],
"when": page["content"]["version"]["when"],
},
"last_modified": page["content"]["version"]["when"],
"author": page["content"]["version"]["by"]["email"],
"labels": [
label["name"] for label in page["content"]["metadata"]["labels"]["results"]
],
}
for page in results["results"]
]
return json.dumps(transformed_results)
@udf
def search_pages(self, query: str, limit: Optional[int] = 10) -> str:
"""Search pages by keyword.
Args:
query: Search string, e.g., 'onboarding guide'
limit: Maximum number of results to return (default: 10)
Returns:
JSON string containing matching pages
"""
logger.info("Searching pages with query: %s, limit: %s", query, limit)
results = self.confluence.cql(cql=f'text ~ "{query}"', limit=limit, expand="version,space")
return json.dumps(results)
@udf
def get_page(self, page_id: str) -> str:
"""Fetch page content (storage format or rendered HTML).
Args:
page_id: Confluence page ID
Returns:
JSON string containing page content
"""
logger.info("Getting page content for ID: %s", page_id)
page = self.confluence.get_page_by_id(page_id=page_id, expand="body.storage,body.view")
return json.dumps(page)
@udf
def get_children(self, page_id: str) -> str:
"""List direct children of a page.
Args:
page_id: Confluence page ID
Returns:
JSON string containing child pages
"""
logger.info("Getting children for page ID: %s", page_id)
children = self.confluence.get_child_pages(page_id=page_id, expand="version,space")
return json.dumps(children)
@udf
def list_spaces(self) -> str:
"""Return all accessible spaces (by key and name).
Returns:
JSON string containing list of spaces
"""
logger.info("Listing all spaces")
spaces = self.confluence.get_all_spaces(expand="description,metadata.labels")
return json.dumps(spaces)
@udf
def describe_page(self, page_id: str) -> str:
"""Show metadata about a page (title, author, updated, labels, etc).
Args:
page_id: Confluence page ID
Returns:
JSON string containing page metadata
"""
logger.info("Getting metadata for page ID: %s", page_id)
page = self.confluence.get_page_by_id(
page_id=page_id, expand="version,space,metadata.labels"
)
return json.dumps(page)

View File

@@ -0,0 +1,2 @@
-- Example CQL query endpoint
SELECT cql_query_confluence($cql, $space_key, $limit) as result;

View File

@@ -0,0 +1,2 @@
-- Show metadata about a Confluence page
SELECT describe_page_confluence($page_id) as result;

View File

@@ -0,0 +1,2 @@
-- List direct children of a Confluence page
SELECT get_children_confluence($page_id) as result;

View File

@@ -0,0 +1,2 @@
-- Get Confluence page content
SELECT get_page_confluence($page_id) as result;

View File

@@ -0,0 +1,2 @@
-- List all accessible Confluence spaces
SELECT list_spaces_confluence() as result;

View File

@@ -0,0 +1,2 @@
-- Search Confluence pages by keyword
SELECT search_pages_confluence($query, $limit) as result;

View File

@@ -0,0 +1,66 @@
mxcp: 1
tool:
name: cql_query
description: "Execute a CQL query against Confluence"
parameters:
- name: cql
type: string
description: |
The CQL query string to execute.
Example: 'text ~ "important documentation"'
examples: [
'text ~ "important documentation"',
'type = page AND space = "TEAM"',
'label = "documentation"'
]
- name: space_key
type: string
description: |
The space key to search in.
Example: 'TEAM'
examples: ["TEAM", "DOCS", "PROD"]
- name: limit
type: integer
description: |
Maximum number of results to return.
Defaults to 10 if not specified.
examples: [10, 20, 50]
return:
type: array
items:
type: object
properties:
id:
type: string
description: "Page ID"
title:
type: string
description: "Page title"
space_key:
type: string
description: "Space key"
url:
type: string
description: "Page URL"
version:
type: object
properties:
number:
type: integer
description: "Version number"
when:
type: string
description: "Version timestamp"
last_modified:
type: string
description: "Last modification timestamp"
author:
type: string
description: "Page author"
labels:
type: array
items:
type: string
description: "Page labels"
source:
file: "../sql/cql_query.sql"

View File

@@ -0,0 +1,28 @@
mxcp: 1
tool:
name: describe_page
description: |
Show metadata about a Confluence page.
Returns a JSON string containing page details like title, author, update date, and labels.
type: tool
annotations:
title: Describe Page
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: page_id
type: string
description: |
The ID of the page to describe.
This is typically a numeric ID found in the page URL.
examples: ["123456", "789012"]
return:
type: string
description: |
A JSON string containing the page metadata.
language: "sql"
source:
file: "../sql/describe_page.sql"

View File

@@ -0,0 +1,28 @@
mxcp: 1
tool:
name: get_children
description: |
List direct children of a Confluence page.
Returns a JSON string containing the child pages.
type: tool
annotations:
title: Get Children
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: page_id
type: string
description: |
The ID of the parent page.
This is typically a numeric ID found in the page URL.
examples: ["123456", "789012"]
return:
type: string
description: |
A JSON string containing an array of child pages.
language: "sql"
source:
file: "../sql/get_children.sql"

View File

@@ -0,0 +1,28 @@
mxcp: 1
tool:
name: get_page
description: |
Fetch a Confluence page's content.
Returns a JSON string containing the page content in both storage format and rendered HTML.
type: tool
annotations:
title: Get Page
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: page_id
type: string
description: |
The ID of the page to fetch.
This is typically a numeric ID found in the page URL.
examples: ["123456", "789012"]
return:
type: string
description: |
A JSON string containing the page content.
language: "sql"
source:
file: "../sql/get_page.sql"

View File

@@ -0,0 +1,21 @@
mxcp: 1
tool:
name: list_spaces
description: |
List all accessible Confluence spaces.
Returns a JSON string containing space keys and names.
type: tool
annotations:
title: List Spaces
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
return:
type: string
description: |
A JSON string containing an array of spaces.
language: "sql"
source:
file: "../sql/list_spaces.sql"

View File

@@ -0,0 +1,38 @@
mxcp: 1
tool:
name: search_pages
description: |
Search Confluence pages by keyword.
Returns a JSON string containing matching pages with their details.
type: tool
annotations:
title: Search Pages
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: query
type: string
description: |
The search string to find in page content.
This will search through page titles and content.
examples: [
"onboarding guide",
"release notes",
"API documentation"
]
- name: limit
type: integer
description: |
Maximum number of results to return.
Defaults to 10 if not specified.
examples: [10, 20, 50]
return:
type: string
description: |
A JSON string containing an array of matching pages.
language: "sql"
source:
file: "../sql/search_pages.sql"

View File

@@ -0,0 +1,4 @@
target/
dbt_packages/
logs/

View File

@@ -0,0 +1,149 @@
# COVID-19 OWID Example
This example demonstrates how to use MXCP to create a COVID-19 data analysis API. It shows how to:
- Fetch and cache COVID-19 data from Our World in Data (OWID)
- Transform data using dbt and DuckDB
- Create a natural language interface for data exploration using generic SQL tools
## Features
- **Comprehensive Data**: Global COVID-19 statistics from OWID
- **Data Transformation**: dbt models for efficient querying
- **Natural Language**: LLM-friendly query interface (prompt only)
## Getting Started
### Prerequisites
Make sure you have the required tools installed:
```bash
# Install MXCP and dependencies
pip install mxcp dbt-core dbt-duckdb
# Option: Install in development mode
cd /path/to/mxcp
python -m venv .venv && source .venv/bin/activate
pip install -e .
```
### Running the Example
1. Navigate to the COVID example:
```bash
cd examples/covid_owid
```
2. Initialize the data:
```bash
dbt deps
dbt run
```
3. Start the MCP server:
```bash
mxcp serve
```
## 🔌 Claude Desktop Integration
To use this example with Claude Desktop:
### 1. Locate Claude's Configuration
Find your Claude Desktop configuration file:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
### 2. Configure the MCP Server
Add this configuration to your `claude_desktop_config.json`:
#### If you installed MXCP globally:
```json
{
"mcpServers": {
"covid": {
"command": "mxcp",
"args": ["serve", "--transport", "stdio"],
"cwd": "/absolute/path/to/mxcp/examples/covid_owid"
}
}
}
```
#### If you're using a virtual environment:
```json
{
"mcpServers": {
"covid": {
"command": "/bin/bash",
"args": [
"-c",
"cd /absolute/path/to/mxcp/examples/covid_owid && source ../../.venv/bin/activate && mxcp serve --transport stdio"
]
}
}
}
```
**Important**: Replace `/absolute/path/to/mxcp` with the actual path to your MXCP installation.
### 3. Restart Claude Desktop
After saving the configuration, restart Claude Desktop to load the new MCP server.
### 4. Test the Integration
In Claude Desktop, try asking:
- "Show me COVID-19 cases in the United States for 2022"
- "Compare vaccination rates between France and Germany"
- "What were the peak hospitalization rates in the UK?"
## 🛠️ Other MCP Clients
This example works with any MCP-compatible tool:
- **mcp-cli**: Interactive command-line interface
- **Custom integrations**: Build your own using the MCP specification
## Example Usage
The LLM can help you analyze:
- Case numbers and death rates
- Vaccination progress
- Hospital occupancy
- Regional comparisons
- Policy effectiveness
All queries are handled through the generic SQL query interface. You can:
- Use `list_tables` to see available tables
- Use `get_table_schema` to inspect table structure
- Use `execute_sql_query` to run custom SQL queries
## Implementation Details
The example uses:
- dbt for data transformation
- DuckDB for efficient storage and querying
- SQL analytics for complex calculations
- Type-safe parameters for filtering
## Project Structure
```
covid_owid/
├── endpoints/ # MCP endpoint definitions
│ └── prompt.yml # LLM system prompt (generic query interface only)
├── models/ # dbt transformations
│ ├── covid_data.sql # Main COVID-19 statistics
│ ├── hospitalizations.sql # Hospital/ICU data
│ └── locations.sql # Geographic data
├── mxcp-site.yml # MCP configuration
└── dbt_project.yml # dbt configuration
```
## Learn More
- [OWID COVID-19 Data](https://github.com/owid/covid-19-data) - Data source
- [dbt Documentation](https://docs.getdbt.com/) - Data transformation
- [DuckDB Documentation](https://duckdb.org/docs/) - Database engine
- [MXCP Documentation](../../docs/quickstart.md) - MCP framework

View File

@@ -0,0 +1,19 @@
analysis-paths:
- analyses
clean-targets:
- target
- dbt_packages
config-version: 2
macro-paths:
- macros
model-paths:
- models
name: covid_owid
profile: covid_owid_default
seed-paths:
- seeds
snapshot-paths:
- snapshots
target-path: target
test-paths:
- tests

View File

@@ -0,0 +1,4 @@
{{ config(materialized='table') }}
select *
from read_csv_auto('https://github.com/owid/covid-19-data/raw/master/public/data/owid-covid-data.csv')

View File

@@ -0,0 +1,5 @@
{{ config(materialized='table') }}
select *
from read_csv_auto('https://github.com/owid/covid-19-data/raw/master/public/data/hospitalizations/covid-hospitalizations.csv')

View File

@@ -0,0 +1,5 @@
{{ config(materialized='table') }}
select *
from read_csv_auto('https://github.com/owid/covid-19-data/raw/master/public/data/hospitalizations/locations.csv')

View File

@@ -0,0 +1,9 @@
version: 2
sources:
- name: github_covid_data
description: "COVID-19 data loaded directly from Our World in Data GitHub repository"
meta:
urls:
owid_covid_data: "https://github.com/owid/covid-19-data/raw/master/public/data/owid-covid-data.csv"
covid_hospitalizations: "https://github.com/owid/covid-19-data/raw/master/public/data/hospitalizations/covid-hospitalizations.csv"

View File

@@ -0,0 +1,5 @@
mxcp: 1
profile: default
project: covid_owid
sql_tools:
enabled: true

View File

@@ -0,0 +1,75 @@
mxcp: 1
prompt:
name: "covid_data_analyst"
description: "An AI assistant that analyzes and explains COVID-19 data from Our World in Data."
tags: ["covid", "analysis", "health", "epidemiology"]
messages:
- role: system
type: text
prompt: |
You are an expert COVID-19 data analyst with access to the Our World in Data (OWID) COVID-19 dataset. You can help users understand and analyze:
1. Case numbers, deaths, and testing data
2. Vaccination rates and their impact
3. Hospital and ICU occupancy rates
4. Regional and country-specific trends
5. Comparative analysis between countries
6. Policy responses and their effectiveness
Data Exploration Tools:
You have access to a generic query interface for exploring the COVID-19 data:
- list_tables: View all available tables in the database
- get_table_schema: Examine the structure and columns of any table
- execute_sql_query: Run custom SQL queries for data analysis
These tools allow you to:
1. Explore available data tables and their structure
2. Create custom queries for specific analysis needs
3. Perform complex aggregations and calculations
4. Combine data from different tables
5. Filter and sort data in any way needed
6. Answer detailed or unusual questions from users
Available data includes:
- Daily and cumulative case counts
- Death rates and mortality statistics
- Testing rates and positivity rates
- Vaccination data (first, second doses, boosters)
- Hospital and ICU admissions
- Demographics and population metrics
- Government response indicators
When responding:
- Use list_tables and get_table_schema to understand available data
- Create focused SQL queries that answer the specific question
- Provide context for the numbers you present
- Explain trends and potential factors affecting the data
- Note any data limitations or gaps
- Use clear, non-technical language when possible
- Cite specific dates and sources
- Acknowledge uncertainty where it exists
- For SQL queries, explain your logic
Example Usage:
1. Explore available tables:
list_tables()
2. Understand table structure:
get_table_schema("covid_data")
3. Custom analysis:
execute_sql_query("
SELECT
location,
date,
new_cases,
new_deaths,
total_vaccinations
FROM covid_data
WHERE date >= '2021-01-01'
AND location IN ('United States', 'United Kingdom')
ORDER BY date DESC
")
The data is sourced from Our World in Data's COVID-19 dataset, which is regularly updated and maintained by researchers at the University of Oxford.

View File

@@ -0,0 +1,139 @@
# Earthquakes Example
This example demonstrates how to use MXCP to create a real-time earthquake data API. It shows how to:
- Query live earthquake data from the USGS API
- Transform JSON data using SQL
- Create type-safe endpoints for LLM consumption
## Features
- **Real-time Data**: Fetches the latest earthquake data from USGS
- **Type Safety**: Strong typing for LLM safety
- **SQL Transformations**: Complex JSON parsing and data transformation
- **Test Coverage**: Includes example tests
## Getting Started
### Prerequisites
Make sure you have MXCP installed:
```bash
# Option 1: Install globally
pip install mxcp
# Option 2: Install in development mode (if you cloned the repo)
cd /path/to/mxcp
python -m venv .venv && source .venv/bin/activate
pip install -e .
```
### Running the Example
1. Navigate to the earthquakes example:
```bash
cd examples/earthquakes
```
2. Start the MCP server:
```bash
mxcp serve
```
## 🔌 Claude Desktop Integration
To use this example with Claude Desktop:
### 1. Locate Claude's Configuration
Find your Claude Desktop configuration file:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
### 2. Configure the MCP Server
Add this configuration to your `claude_desktop_config.json`:
#### If you installed MXCP globally:
```json
{
"mcpServers": {
"earthquakes": {
"command": "mxcp",
"args": ["serve", "--transport", "stdio"],
"cwd": "/absolute/path/to/mxcp/examples/earthquakes"
}
}
}
```
#### If you're using a virtual environment:
```json
{
"mcpServers": {
"earthquakes": {
"command": "/bin/bash",
"args": [
"-c",
"cd /absolute/path/to/mxcp/examples/earthquakes && source ../../.venv/bin/activate && mxcp serve --transport stdio"
]
}
}
}
```
**Important**: Replace `/absolute/path/to/mxcp` with the actual path to your MXCP installation.
### 3. Restart Claude Desktop
After saving the configuration, restart Claude Desktop to load the new MCP server.
### 4. Test the Integration
In Claude Desktop, try asking:
- "Show me recent earthquakes above magnitude 5.0"
- "What was the strongest earthquake in the last 24 hours?"
- "List earthquakes near California"
Claude will automatically use the earthquake data tools to answer your questions.
## 🛠️ Other MCP Clients
This example also works with other MCP-compatible tools:
- **mcp-cli**: `pip install mcp-cli` then use the same server config
- **Custom integrations**: Use the MCP specification to build your own client
## Example Usage
Ask your LLM to:
- "Show me recent earthquakes above magnitude 5.0"
- "What was the strongest earthquake in the last 24 hours?"
- "List earthquakes near [location]"
## Implementation Details
The example uses:
- DuckDB's `read_json_auto` function to parse USGS GeoJSON
- SQL window functions for data analysis
- Type-safe parameters for filtering
For more details on:
- Type system: See [Type System Documentation](../../docs/type-system.md)
- SQL capabilities: See [Integrations Documentation](../../docs/integrations.md)
- Configuration: See [Configuration Guide](../../docs/configuration.md)
## Project Structure
```
earthquakes/
├── endpoints/
│ └── tool.yml # Endpoint definition
├── mxcp-site.yml # Project configuration
└── tests/ # Example tests
```
## Learn More
- [Quickstart Guide](../../docs/quickstart.md) - Get started with MXCP
- [CLI Reference](../../docs/cli.md) - Available commands
- [Configuration](../../docs/configuration.md) - Project setup

View File

@@ -0,0 +1,7 @@
mxcp: 1
project: earthquake-api
profile: prod
profiles:
prod:
audit:
enabled: true

View File

@@ -0,0 +1,25 @@
mxcp: 1
prompt:
name: "summarize_earthquake_data"
description: "Summarizes recent earthquake activity in plain English."
tags: ["summary", "earthquake"]
parameters:
- name: top_event
type: string
description: "The most significant recent earthquake details as text"
messages:
- role: system
type: text
prompt: "You are an expert seismologist summarizing recent activity for the general public."
- role: user
type: text
prompt: |
Based on this recent event: {{ top_event }},
please provide a brief summary of current seismic activity.
Explain the significance of the event in terms of its magnitude and location.
Include any additional details that might help the user understand the event.
Use simple language and avoid technical terms.
Keep it short and concise.
Example output:
"There was a magnitude 5.5 earthquake in San Francisco yesterday."

View File

@@ -0,0 +1,52 @@
mxcp: 1
tool:
name: "query_recent_earthquakes"
description: "Query earthquakes over a given magnitude threshold."
tags: ["earthquake", "filter"]
parameters:
- name: min_magnitude
type: number
description: "Minimum magnitude"
default: 2.5
return:
type: array
items:
type: object
source:
code: |
WITH raw AS (
SELECT * FROM read_json_auto('https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson')
),
features AS (
SELECT
feature
FROM raw,
UNNEST(features) AS feature
),
quakes AS (
SELECT
feature -> 'unnest' -> 'properties' -> 'mag' AS magnitude,
feature -> 'unnest' -> 'properties' -> 'place' AS location,
feature -> 'unnest' -> 'properties' -> 'time' AS time,
feature -> 'unnest' -> 'geometry' -> 'coordinates' AS coords
FROM features
)
SELECT
CAST(magnitude AS DOUBLE) AS magnitude,
location,
CAST(time AS BIGINT) AS time,
coords
FROM quakes
WHERE CAST(magnitude AS DOUBLE) >= $min_magnitude
ORDER BY magnitude DESC;
annotations:
title: "Query Significant Earthquakes"
readOnlyHint: true
idempotentHint: true
openWorldHint: true
tests:
- name: filter-mag
arguments:
- key: min_magnitude
value: 5.5

View File

@@ -0,0 +1,213 @@
# Google Calendar OAuth Demo (Read-Only)
This example demonstrates how to create safe, read-only MCP tools that interact with Google Calendar using the MXCP OAuth authentication system with the Google Calendar API.
## Features Demonstrated
### 1. MXCP OAuth Authentication
- Project-wide Google OAuth configuration
- Automatic token management through MXCP authentication system
- User authentication via standard OAuth 2.0 flow
- Error handling for authentication failures
### 2. Google Calendar API Integration (Read-Only)
- `whoami` - Display information about the current authenticated Google user
- `list_calendars` - Retrieve all accessible calendars with filtering options
- `get_calendar` - Get detailed information for a specific calendar
- `list_events` - List events from a calendar with time filtering and pagination
- `get_event` - Retrieve detailed information for a specific event
- `search_events` - Search for events matching text queries
- `get_freebusy` - Check availability across multiple calendars
- Token-based API access using authenticated user context
- **Safe Design**: Only read operations - no calendar or event modifications
## Prerequisites
1. **Google Account**: You need a Google account with Calendar access
2. **Google Cloud Project**: Create a project in Google Cloud Console with Calendar API enabled
3. **OAuth Credentials**: Create OAuth 2.0 credentials for your application
4. **Python Dependencies**: The `google-api-python-client` and related libraries (automatically managed by MXCP)
## Setup
### 1. Create Google Cloud Project and Enable APIs
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the Google Calendar API:
- Go to **APIs & Services****Library**
- Search for "Google Calendar API"
- Click on it and press **Enable**
### 2. Configure OAuth Consent Screen (Required First)
1. In Google Cloud Console, go to **APIs & Services****OAuth consent screen**
2. Configure the consent screen:
- **User Type**: External (for testing) or Internal (for organization use)
- **App Name**: "MXCP Google Calendar Integration" (or your preferred name)
- **User Support Email**: Your email
- **Developer Contact**: Your email
3. **Add Scopes** (under "Data access" section):
- Click "Add or Remove Scopes"
- In the scope selection dialog, search for "calendar"
- Find and select `https://www.googleapis.com/auth/calendar.readonly` (Calendar read-only access)
- Click "Update" to save the scopes
4. Save the consent screen configuration
**Note**: The scopes are configured in the OAuth Consent Screen, not when creating the Client ID. This is why you don't see scope options when creating credentials.
### 3. Create OAuth 2.0 Client ID
1. Go to **APIs & Services****Credentials**
2. Click **Create Credentials****OAuth 2.0 Client IDs**
3. Configure the client:
- **Application Type**: Web application
- **Name**: "MXCP Calendar Client" (or your preferred name)
- **Authorized Redirect URIs**: Add based on your deployment:
- **Local Development**: `http://localhost:8000/google/callback`
- **Remote/Production**: `https://your-domain.com/google/callback` (replace with your actual server URL)
4. Save and note down the **Client ID** and **Client Secret**
### 4. Configure Environment Variables
Set your Google OAuth credentials:
```bash
export GOOGLE_CLIENT_ID="your-client-id-from-google-cloud"
export GOOGLE_CLIENT_SECRET="your-client-secret-from-google-cloud"
```
### 5. Configure Callback URL for Your Deployment
The callback URL configuration depends on where your MXCP server will run:
#### Local Development
For local development, the default configuration in `config.yml` uses `http://localhost:8000/google/callback`. This works when:
- You're running MXCP locally on your development machine
- Users authenticate from the same machine where MXCP is running
#### Remote/Production Deployment
For remote servers or production deployments, you need to:
1. **Update config.yml**: Modify the callback URL:
```yaml
redirect_uris:
- "https://your-domain.com/google/callback" # Your actual URL
```
2. **Update base_url**: Set the correct base URL in your config:
```yaml
transport:
http:
base_url: https://your-domain.com # Your actual server URL
```
3. **Configure OAuth Credentials**: Add the production callback URL to your Google Cloud OAuth credentials
**Important**:
- The callback URL must be accessible from the user's browser, not just from your server
- For production deployments, Google requires HTTPS for callback URLs
- You can configure multiple callback URLs in your OAuth credentials to support both local development and production
## Project Structure
```
google-calendar/
├── mxcp-site.yml # Project metadata
├── config.yml # Server and authentication configuration
├── python/ # Python modules
│ └── google_calendar_client.py # Google Calendar API implementations
├── tools/ # Tool definitions (read-only)
│ ├── whoami.yml # Current user information
│ ├── list_calendars.yml # List accessible calendars
│ ├── get_calendar.yml # Get calendar details
│ ├── list_events.yml # List calendar events
│ ├── get_event.yml # Get event details
│ ├── search_events.yml # Search for events
│ └── get_freebusy.yml # Check availability
└── README.md # This file
```
## Key Concepts
1. **MXCP OAuth Integration**: Uses MXCP's built-in Google OAuth provider for secure authentication
2. **User Context**: Access tokens are automatically managed and provided through `get_user_context()`
3. **Token-based Authentication**: Google API client is initialized with OAuth tokens instead of service account credentials
4. **Project-wide Configuration**: Authentication is configured at the project level in `config.yml`
5. **Error Handling**: Comprehensive error handling for authentication and API failures
6. **Type Safety**: Uses Python type hints and comprehensive error handling for data validation
## Running the Example
Once you've completed the setup above:
1. **Start MXCP**:
```bash
# From the examples/google-calendar directory:
MXCP_CONFIG=config.yml mxcp serve
```
2. **Connect your MCP client** (e.g., Claude Desktop) to the MXCP server
3. **Authenticate**: When the client first connects, you'll be redirected to Google to authorize the application
4. **Use the tools**: Once authenticated, you can use all the Google Calendar tools through your MCP client
## Example Usage
When you use the tools through an MCP client, you can:
### Get User Information
```
Use the whoami tool to see your Google profile information
```
### Manage Calendars
```
List all your calendars, get details for specific calendars, and check which ones you can modify
```
### View Calendar Events
```
- List events: "What's on my calendar this week?"
- Search events: "Find all meetings with John"
- Get event details: "Show me details for my 3 PM meeting"
- View event information: "What meetings do I have with the marketing team?"
```
### Check Availability
```
Use the freebusy tool to find available time slots across multiple calendars
```
## Troubleshooting
### Authentication Errors
- **"No user context available"**: User needs to authenticate first by running `mxcp serve` and completing OAuth flow
- **"No Google access token found"**: Authentication was incomplete or token expired - re-authenticate
- **OAuth Credentials Issues**: Verify your `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are correct
- **Callback URL Mismatch**: Ensure the callback URL in your Google Cloud OAuth credentials matches where your MXCP server is accessible
- **API Not Enabled**: Make sure the Google Calendar API is enabled in your Google Cloud project
### API Errors
- **403 Forbidden**: Check that the Calendar API is enabled and your OAuth scopes include calendar access
- **404 Not Found**: Verify calendar IDs and event IDs are correct and accessible to the authenticated user
- **Rate Limiting**: Google Calendar API has rate limits - implement appropriate retry logic if needed
### OAuth Setup Issues
- **Consent Screen**: Make sure your OAuth consent screen is properly configured with the correct scopes
- **Redirect URI**: The redirect URI must exactly match your MXCP server's accessible address
- **Scopes**: Ensure your OAuth configuration includes `https://www.googleapis.com/auth/calendar.readonly` scope
## Next Steps
This example demonstrates a comprehensive set of read-only Google Calendar integration tools. You could extend it with additional features like:
- Advanced calendar filtering and search capabilities
- Integration with other Google Workspace services (read-only)
- Calendar analytics and reporting
- Event pattern analysis and insights
- Multi-calendar comparison and availability analysis
**Note**: This example is intentionally read-only for safety. If you need write operations (create, update, delete), you would need to:
- Change the OAuth scope to `https://www.googleapis.com/auth/calendar` (full access)
- Add appropriate write functions with proper validation and error handling
- Implement additional safety measures and user confirmations

View File

@@ -0,0 +1,22 @@
mxcp: 1
transport:
http:
port: 8000
host: 0.0.0.0
# Set base_url to your server's public URL for production
base_url: http://localhost:8000
projects:
google-calendar:
profiles:
default:
# OAuth Authentication Configuration
auth:
provider: google
google:
client_id: "${GOOGLE_CLIENT_ID}"
client_secret: "${GOOGLE_CLIENT_SECRET}"
scope: "https://www.googleapis.com/auth/calendar.readonly https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email"
callback_path: "/google/callback"
auth_url: "https://accounts.google.com/o/oauth2/v2/auth"
token_url: "https://oauth2.googleapis.com/token"

View File

@@ -0,0 +1,3 @@
mxcp: 1
project: google-calendar
profile: default

View File

@@ -0,0 +1,804 @@
"""
Google Calendar MCP client implementation using mxcp OAuth authentication.
This module provides Google Calendar API integration with:
- OAuth 2.0 authentication via mxcp framework
- Thread-safe client caching for performance
- Simplified time handling for LLM consumption
- Comprehensive error handling and user-friendly messages
- Full type safety with Pydantic models
"""
# Required for union syntax (|) in type annotations with runtime objects like threading.Lock
# Without this, Python tries to evaluate "threading.Lock | None" at runtime, which fails
from __future__ import annotations
import logging
import threading
from datetime import date, datetime, timezone
from functools import wraps
from typing import Any
from google.auth.exceptions import RefreshError
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import Resource, build # type: ignore[import-untyped]
from googleapiclient.errors import HttpError # type: ignore[import-untyped]
from mxcp.runtime import on_init, on_shutdown
from mxcp.sdk.auth.context import get_user_context
# =============================================================================
# TIME CONVERSION UTILITIES
# =============================================================================
def _datetime_to_google_time(
dt: datetime, all_day: bool = False, time_zone: str | None = None
) -> dict[str, Any]:
"""
Convert datetime object to Google Calendar API time format.
Args:
dt: Python datetime object (should be timezone-aware)
all_day: Whether this represents an all-day event
time_zone: Optional timezone override
Returns:
Google API time object: {"dateTime": "...", "timeZone": "..."}
or {"date": "YYYY-MM-DD"} for all-day events
"""
if all_day:
# For all-day events, use date format
return {"date": dt.date().isoformat()}
else:
# For timed events, use dateTime format
time_obj = {"dateTime": dt.isoformat()}
# Add timezone if specified or if datetime has timezone info
if time_zone:
time_obj["timeZone"] = time_zone
elif dt.tzinfo:
# Extract timezone name from datetime object if possible
tz_name = getattr(dt.tzinfo, "zone", None) or str(dt.tzinfo)
if tz_name != "UTC" and "+" not in tz_name and "-" not in tz_name:
time_obj["timeZone"] = tz_name
else:
# If no timezone info is available, try to get user's timezone
try:
user_timezone = _get_user_timezone()
logger.warning(
f"Datetime object has no timezone info, using user timezone: {user_timezone}"
)
time_obj["timeZone"] = user_timezone
except ValueError as e:
raise ValueError(
f"Datetime object has no timezone information and cannot determine timezone from calendar. "
f"Please either: 1) Use timezone-aware datetime objects, or 2) Specify the time_zone parameter. "
f"Original error: {e}"
) from e
return time_obj
def _get_user_timezone() -> str:
"""Get the user's timezone from their primary calendar.
This function only tries to get the timezone from the user's primary calendar.
If that fails, it raises an exception to force explicit timezone specification.
Results are cached to avoid repeated lookups.
Returns:
IANA timezone identifier from user's primary calendar
Raises:
ValueError: If timezone cannot be determined from primary calendar
"""
global _user_timezone_cache, _timezone_cache_lock
# Check cache first
if _timezone_cache_lock and _user_timezone_cache:
with _timezone_cache_lock:
if _user_timezone_cache:
return _user_timezone_cache
try:
# Get timezone from user's primary calendar
calendar_info = get_calendar("primary")
if calendar_info and calendar_info.get("timeZone"):
user_timezone: str = calendar_info["timeZone"]
logger.debug(f"Using timezone from primary calendar: {user_timezone}")
# Cache the result
if _timezone_cache_lock:
with _timezone_cache_lock:
_user_timezone_cache = user_timezone
return user_timezone
else:
raise ValueError("Primary calendar does not have timezone information")
except Exception as e:
logger.debug(f"Could not get timezone from primary calendar: {e}")
raise ValueError(
"Cannot determine timezone from primary calendar. "
"Please specify the time_zone parameter explicitly in your function call."
) from e
def _google_time_to_datetime(google_time: dict[str, Any]) -> tuple[datetime, bool]:
"""
Convert Google Calendar API time format to datetime object.
Args:
google_time: Google API time object
Returns:
Tuple of (datetime_object, is_all_day)
"""
if "date" in google_time:
# All-day event - convert date to datetime at midnight UTC
date_obj = date.fromisoformat(google_time["date"])
dt = datetime.combine(date_obj, datetime.min.time(), tzinfo=timezone.utc)
return dt, True
elif "dateTime" in google_time:
# Timed event - parse ISO datetime
dt = datetime.fromisoformat(google_time["dateTime"])
return dt, False
else:
raise ValueError(f"Invalid Google time format: {google_time}")
def _convert_event_to_simplified(google_event: dict[str, Any]) -> dict[str, Any]:
"""
Convert Google Calendar API event to our simplified EventInfo format.
Args:
google_event: Raw event from Google Calendar API
Returns:
Event in simplified format matching EventInfo model
"""
# Convert start/end times
start_dt, start_all_day = _google_time_to_datetime(google_event["start"])
end_dt, end_all_day = _google_time_to_datetime(google_event["end"])
# Ensure both times have same all-day status
all_day = start_all_day and end_all_day
simplified_event = {
"id": google_event["id"],
"summary": google_event.get("summary", ""),
"description": google_event.get("description"),
"location": google_event.get("location"),
"start_time": start_dt,
"end_time": end_dt,
"all_day": all_day,
"time_zone": google_event.get("start", {}).get("timeZone"),
"status": google_event.get("status", "confirmed"),
"htmlLink": google_event.get("htmlLink", ""),
"created": datetime.fromisoformat(google_event["created"].replace("Z", "+00:00")),
"updated": datetime.fromisoformat(google_event["updated"].replace("Z", "+00:00")),
"calendar_id": google_event.get("calendarId", "unknown"), # Added by our code
"etag": google_event.get("etag"),
}
# Convert attendees if present
if "attendees" in google_event:
simplified_event["attendees"] = [
{
"email": att["email"],
"displayName": att.get("displayName"),
"responseStatus": att.get("responseStatus", "needsAction"),
"optional": att.get("optional", False),
"resource": att.get("resource", False),
"comment": att.get("comment"),
"additionalGuests": att.get("additionalGuests", 0),
}
for att in google_event["attendees"]
]
# Convert creator/organizer if present
for role in ["creator", "organizer"]:
if role in google_event:
simplified_event[role] = {
"email": google_event[role].get("email"),
"displayName": google_event[role].get("displayName"),
"self": google_event[role].get("self", False),
}
# Handle recurrence rules
if "recurrence" in google_event:
simplified_event["recurrence"] = google_event["recurrence"]
# Handle reminders
if "reminders" in google_event:
reminders = google_event["reminders"]
simplified_event["reminders"] = {
"useDefault": reminders.get("useDefault", True),
"overrides": reminders.get("overrides"),
}
# Handle transparency and visibility
simplified_event["transparency"] = google_event.get("transparency", "opaque")
simplified_event["visibility"] = google_event.get("visibility", "default")
return simplified_event
def _convert_simplified_to_google_event(simplified_event: dict[str, Any]) -> dict[str, Any]:
"""
Convert simplified event format to Google Calendar API format.
Args:
simplified_event: Event in our simplified format
Returns:
Event in Google Calendar API format
"""
google_event = {
"summary": simplified_event["summary"],
"start": _datetime_to_google_time(
simplified_event["start_time"],
simplified_event.get("all_day", False),
simplified_event.get("time_zone"),
),
"end": _datetime_to_google_time(
simplified_event["end_time"],
simplified_event.get("all_day", False),
simplified_event.get("time_zone"),
),
}
# Add optional fields
optional_fields = ["description", "location"]
for field in optional_fields:
if field in simplified_event and simplified_event[field] is not None:
google_event[field] = simplified_event[field]
# Convert attendees
if "attendees" in simplified_event and simplified_event["attendees"]:
google_event["attendees"] = [
(
{"email": str(email)}
if isinstance(email, str)
else {
"email": (
str(email) if isinstance(email, str) else str(attendee.get("email", ""))
),
"displayName": attendee.get("displayName"),
"optional": attendee.get("optional", False),
"resource": attendee.get("resource", False),
"comment": attendee.get("comment"),
"additionalGuests": attendee.get("additionalGuests", 0),
}
)
for email in simplified_event["attendees"]
for attendee in [email if isinstance(email, dict) else {"email": email}]
]
# Add transparency and visibility if specified
if "transparency" in simplified_event:
google_event["transparency"] = simplified_event["transparency"]
if "visibility" in simplified_event:
google_event["visibility"] = simplified_event["visibility"]
if "recurrence" in simplified_event:
google_event["recurrence"] = simplified_event["recurrence"]
# Add reminders if specified
if "reminders" in simplified_event and simplified_event["reminders"]:
google_event["reminders"] = simplified_event["reminders"]
return google_event
# =============================================================================
# THREAD-SAFE CLIENT CACHING
# =============================================================================
# Thread-safe cache for Google Calendar service clients
_client_cache: dict[str, Resource] | None = None
_cache_lock: threading.Lock | None = None
# Global timezone cache to avoid repeated lookups
_user_timezone_cache: str | None = None
_timezone_cache_lock: threading.Lock | None = None
@on_init
def init_client_cache() -> None:
"""Initialize the Google Calendar client cache and timezone cache."""
global _client_cache, _cache_lock, _timezone_cache_lock
_client_cache = {}
_cache_lock = threading.Lock()
_timezone_cache_lock = threading.Lock()
@on_shutdown
def clear_client_cache() -> None:
"""Clear the Google Calendar client cache and timezone cache."""
global _client_cache, _cache_lock, _user_timezone_cache, _timezone_cache_lock
_client_cache = None
_cache_lock = None
_user_timezone_cache = None
_timezone_cache_lock = None
def _get_cache_key(context: Any) -> str | None:
"""Generate cache key based on user context."""
if not context:
return None
# Use user ID as cache key for per-user client isolation
user_id = getattr(context, "user_id", None) or getattr(context, "id", None)
if user_id:
return f"gcal:{user_id}"
return None
def _get_google_credentials() -> Credentials:
"""Get Google OAuth credentials from mxcp user context."""
context = get_user_context()
if not context or not context.external_token:
raise ValueError("No user context available. User must be authenticated.")
# Create Google credentials object with OAuth token
credentials = Credentials(token=context.external_token) # type: ignore[no-untyped-call]
return credentials
# =============================================================================
# LOGGING & ERROR HANDLING
# =============================================================================
# Set up comprehensive logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def with_session_retry(func: Any) -> Any:
"""
Decorator for handling OAuth token refresh and API errors with user-friendly messages.
Wraps functions to automatically handle:
- OAuth token refresh failures (RefreshError)
- Google API HTTP errors with specific status codes
- Client cache invalidation on auth failures
- Comprehensive error logging for debugging
"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
logger.info(f"Executing {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
logger.info(f"Successfully completed {func.__name__}")
return result
except RefreshError as e:
# OAuth token has expired and cannot be refreshed
logger.warning(f"OAuth token refresh failed in {func.__name__}: {e}")
clear_client_cache() # Clear cache to force re-authentication
error_msg = (
"Your Google Calendar access has expired. Please re-authenticate to continue."
)
logger.error(f"Authentication error: {error_msg}")
raise ValueError(error_msg) from e
except HttpError as e:
# Handle specific Google API errors with detailed logging
status = e.resp.status
error_details = str(e)
logger.error(
f"Google API HttpError in {func.__name__}: status={status}, details={error_details}"
)
# Clear cache on authentication errors
if status in [401, 403]:
context = get_user_context()
cache_key = _get_cache_key(context)
if cache_key and _cache_lock and _client_cache:
with _cache_lock:
_client_cache.pop(cache_key, None)
# Just forward the original Google API error - it's already clear and actionable
raise ValueError(f"Google Calendar API error: {error_details}") from e
# ValidationError removed since we no longer use Pydantic models
except ValueError as e:
# Re-raise ValueError (these are user-friendly messages)
logger.warning(f"User error in {func.__name__}: {e}")
raise
except Exception as e:
# Catch-all for unexpected errors
logger.error(
f"Unexpected error in {func.__name__}: {type(e).__name__}: {e}", exc_info=True
)
raise ValueError(f"An unexpected error occurred: {str(e)}") from e
return wrapper
def _get_google_calendar_client() -> Resource:
"""
Get cached Google Calendar API client or create new one with OAuth authentication.
Uses per-user caching for performance and proper multi-user isolation.
"""
try:
# Get authenticated user context
context = get_user_context()
if not context:
raise ValueError("No user context available. User must be authenticated.")
# Check cache first
cache_key = _get_cache_key(context)
if cache_key and _cache_lock and _client_cache:
with _cache_lock:
if cache_key in _client_cache:
logging.info("Using cached Google Calendar client")
return _client_cache[cache_key]
logging.info("Creating new Google Calendar client")
# Create new authenticated client
credentials = _get_google_credentials()
service = build(
serviceName="calendar",
version="v3",
credentials=credentials,
cache_discovery=True, # Cache API discovery documents
num_retries=3, # Retry transient failures
)
# Cache the client for this user
if cache_key and _cache_lock and _client_cache:
with _cache_lock:
_client_cache[cache_key] = service
return service
except Exception as e:
raise ValueError(f"Failed to initialize Google Calendar client: {str(e)}") from e
# =============================================================================
# MCP TOOL FUNCTIONS
# =============================================================================
@with_session_retry
def whoami() -> dict[str, Any]:
"""
Get information about the currently authenticated user.
Returns:
UserInfo object with user profile data from Google OAuth
Note:
Uses OAuth profile information, no additional API calls required
"""
context = get_user_context()
if not context:
raise ValueError("No user context available. User must be authenticated.")
# Extract user information from OAuth profile
raw_profile = context.raw_profile or {}
# Return plain dictionary following standard MXCP pattern
return {
"id": raw_profile.get("sub") or raw_profile.get("id", "unknown"),
"email": raw_profile.get("email", "unknown@example.com"),
"name": raw_profile.get("name", "Unknown User"),
"given_name": raw_profile.get("given_name"),
"family_name": raw_profile.get("family_name"),
"picture": raw_profile.get("picture"),
"locale": raw_profile.get("locale"),
"verified_email": raw_profile.get("email_verified"),
}
@with_session_retry
def list_calendars(
show_hidden: bool = False,
show_deleted: bool = False,
max_results: int = 100,
min_access_role: str | None = None,
) -> list[dict[str, Any]]:
"""
List all calendars accessible to the authenticated user.
Args:
show_hidden: Include hidden calendars in results
show_deleted: Include deleted calendars in results
max_results: Maximum number of calendars to return (1-250)
min_access_role: Filter by minimum access level
Returns:
List of CalendarInfo objects with user's accessible calendars
Raises:
ValueError: If user is not authenticated or parameters are invalid
"""
service = _get_google_calendar_client()
# Build parameters
params: dict[str, Any] = {
"maxResults": min(max_results, 250),
"showHidden": show_hidden,
"showDeleted": show_deleted,
}
if min_access_role:
params["minAccessRole"] = min_access_role
# Execute API call
result = service.calendarList().list(**params).execute()
# Return plain dictionaries following standard MXCP pattern
calendars = []
for cal in result.get("items", []):
calendar_dict = {
"id": cal["id"],
"summary": cal.get("summary", ""),
"description": cal.get("description"),
"timeZone": cal.get("timeZone", "UTC"),
"accessRole": cal.get("accessRole", "reader"),
"primary": cal.get("primary", False),
"backgroundColor": cal.get("backgroundColor"),
"foregroundColor": cal.get("foregroundColor"),
"selected": cal.get("selected", False),
"hidden": cal.get("hidden", False),
"defaultReminders": cal.get("defaultReminders"),
}
calendars.append(calendar_dict)
return calendars
@with_session_retry
def get_calendar(calendar_id: str) -> dict[str, Any]:
"""
Get detailed information for a specific calendar.
Args:
calendar_id: Calendar identifier or 'primary' for main calendar
Returns:
CalendarInfo object with calendar details
Raises:
ValueError: If calendar_id is invalid or user lacks access
"""
service = _get_google_calendar_client()
try:
result = service.calendarList().get(calendarId=calendar_id).execute()
except HttpError as e:
if e.resp.status == 404:
raise ValueError(
f"Calendar '{calendar_id}' not found or you don't have access to it"
) from e
raise
# Return plain dictionary following standard MXCP pattern
return {
"id": result["id"],
"summary": result.get("summary", ""),
"description": result.get("description"),
"timeZone": result.get("timeZone", "UTC"),
"accessRole": result.get("accessRole", "reader"),
"primary": result.get("primary", False),
"backgroundColor": result.get("backgroundColor"),
"foregroundColor": result.get("foregroundColor"),
"selected": result.get("selected", False),
"hidden": result.get("hidden", False),
"defaultReminders": result.get("defaultReminders"),
}
@with_session_retry
def list_events(
calendar_id: str = "primary",
time_min: datetime | None = None,
time_max: datetime | None = None,
max_results: int = 250,
single_events: bool = True,
order_by: str = "startTime",
page_token: str | None = None,
) -> dict[str, Any]:
"""
List events from a specific calendar with optional time filtering.
Args:
calendar_id: Calendar to query ('primary' or specific calendar ID)
time_min: Lower bound for event start times (inclusive)
time_max: Upper bound for event start times (exclusive)
max_results: Maximum number of events to return (1-2500)
single_events: Whether to expand recurring events into instances
order_by: Sort order - 'startTime' or 'updated'
page_token: Token for pagination
Returns:
EventSearchResult with events and pagination info
"""
service = _get_google_calendar_client()
# Build parameters
params = {
"calendarId": calendar_id,
"maxResults": min(max_results, 2500),
"singleEvents": single_events,
"orderBy": order_by,
}
if time_min:
params["timeMin"] = time_min.isoformat()
if time_max:
params["timeMax"] = time_max.isoformat()
if page_token:
params["pageToken"] = page_token
# Execute API call
result = service.events().list(**params).execute()
# Convert events to simplified format
events = []
for event in result.get("items", []):
event["calendarId"] = calendar_id # Add calendar_id to event
simplified_event = _convert_event_to_simplified(event)
events.append(simplified_event)
# Return plain dictionary following standard MXCP pattern
return {
"events": events,
"next_page_token": result.get("nextPageToken"),
"total_results": len(events),
}
@with_session_retry
def get_event(calendar_id: str, event_id: str) -> dict[str, Any]:
"""
Retrieve detailed information for a specific event.
Args:
calendar_id: Calendar containing the event
event_id: Event identifier
Returns:
EventInfo object with complete event details
Raises:
ValueError: If event not found or user lacks access
"""
service = _get_google_calendar_client()
try:
result = service.events().get(calendarId=calendar_id, eventId=event_id).execute()
except HttpError as e:
if e.resp.status == 404:
raise ValueError(f"Event '{event_id}' not found in calendar '{calendar_id}'") from e
raise
# Add calendar_id to event
result["calendarId"] = calendar_id
simplified_event = _convert_event_to_simplified(result)
return simplified_event
@with_session_retry
def search_events(
q: str,
calendar_id: str = "primary",
time_min: datetime | None = None,
time_max: datetime | None = None,
max_results: int = 250,
page_token: str | None = None,
) -> dict[str, Any]:
"""
Search for events matching a text query.
Args:
q: Free text search query (searches title, description, location, attendees)
calendar_id: Calendar to search ('primary' or specific calendar ID)
time_min: Earliest event start time to include
time_max: Latest event start time to include
max_results: Maximum number of events to return
page_token: Token for pagination
Returns:
EventSearchResult with matching events and pagination info
"""
service = _get_google_calendar_client()
# Build parameters
params = {
"calendarId": calendar_id,
"q": q,
"maxResults": min(max_results, 2500),
"singleEvents": True,
"orderBy": "startTime",
}
if time_min:
params["timeMin"] = time_min.isoformat()
if time_max:
params["timeMax"] = time_max.isoformat()
if page_token:
params["pageToken"] = page_token
# Execute API call
result = service.events().list(**params).execute()
# Convert events to simplified format
events = []
for event in result.get("items", []):
event["calendarId"] = calendar_id # Add calendar_id to event
simplified_event = _convert_event_to_simplified(event)
events.append(simplified_event)
# Return plain dictionary following standard MXCP pattern
return {
"events": events,
"next_page_token": result.get("nextPageToken"),
"total_results": len(events),
}
@with_session_retry
def get_freebusy(calendar_ids: list[str], time_min: datetime, time_max: datetime) -> dict[str, Any]:
"""
Check free/busy status across multiple calendars.
Args:
calendar_ids: List of calendar IDs to check (use 'primary' for main calendar)
time_min: Start time for availability check
time_max: End time for availability check
Returns:
FreeBusyResponse with busy periods for each calendar
Note:
Useful for finding meeting slots and checking availability before scheduling
"""
if time_min >= time_max:
raise ValueError("time_max must be after time_min")
service = _get_google_calendar_client()
# Build request
request_body = {
"timeMin": time_min.isoformat(),
"timeMax": time_max.isoformat(),
"items": [{"id": cal_id} for cal_id in calendar_ids],
}
# Execute API call
result = service.freebusy().query(body=request_body).execute()
# Convert to plain dictionary format following standard MXCP pattern
calendars = []
for calendar_id in calendar_ids:
calendar_data = result.get("calendars", {}).get(calendar_id, {})
# Convert busy times to plain dictionaries
busy_times = []
for busy_period in calendar_data.get("busy", []):
busy_times.append(
{
"start": busy_period["start"],
"end": busy_period["end"],
}
)
calendars.append(
{"calendar_id": calendar_id, "busy": busy_times, "errors": calendar_data.get("errors")}
)
return {
"time_min": time_min.isoformat(),
"time_max": time_max.isoformat(),
"calendars": calendars,
}

View File

@@ -0,0 +1,63 @@
mxcp: 1
tool:
name: get_calendar
title: Get Calendar Details
description: |
Get detailed information for a specific calendar by ID.
Returns calendar metadata including timezone, access role, and display properties.
Example usage:
- "Get details for my primary calendar"
- "Show me information about the work@company.com calendar"
- "What timezone is my calendar set to?"
tags:
- google-calendar
- calendars
- get
annotations:
readOnlyHint: true
idempotentHint: true
parameters:
- name: calendar_id
type: string
description: "Calendar identifier or 'primary' for main calendar"
default: "primary"
examples: ["primary", "work@company.com", "team@company.com"]
return:
type: object
description: "Calendar details and metadata"
properties:
id:
type: string
description: "Calendar identifier"
summary:
type: string
description: "Calendar name/title"
description:
type: string
description: "Calendar description"
timeZone:
type: string
description: "IANA timezone identifier"
accessRole:
type: string
description: "User's access level"
primary:
type: boolean
description: "Whether this is user's primary calendar"
backgroundColor:
type: string
description: "Background color hex code"
foregroundColor:
type: string
description: "Foreground color hex code"
selected:
type: boolean
description: "Whether calendar is selected in UI"
hidden:
type: boolean
description: "Whether calendar is hidden from list"
required: ["id", "summary", "timeZone", "accessRole"]
language: python
source:
file: ../python/google_calendar_client.py

View File

@@ -0,0 +1,96 @@
mxcp: 1
tool:
name: get_event
title: Get Event Details
description: |
Retrieve detailed information for a specific event by ID.
Returns complete event data in simplified format optimized for LLM use.
Example usage:
- "Show me details for event abc123 in my primary calendar"
- "Get full information about that meeting I mentioned"
- "What are the attendees for event xyz789?"
tags:
- google-calendar
- events
- get
annotations:
readOnlyHint: true
idempotentHint: true
parameters:
- name: calendar_id
type: string
description: "Calendar containing the event"
examples: ["primary", "work@company.com", "team-calendar@company.com"]
- name: event_id
type: string
description: "Event identifier"
examples: ["abc123def456", "event_id_example", "recurring_event_20240115T090000Z"]
return:
type: object
description: "Complete event details"
properties:
id:
type: string
description: "Event identifier"
summary:
type: string
description: "Event title"
description:
type: string
description: "Event description"
location:
type: string
description: "Event location"
start_time:
type: string
format: date-time
description: "Event start time"
end_time:
type: string
format: date-time
description: "Event end time"
all_day:
type: boolean
description: "Whether this is an all-day event"
time_zone:
type: string
description: "Event timezone"
attendees:
type: array
description: "Event attendees"
items:
type: object
properties:
email:
type: string
description: "Attendee email"
displayName:
type: string
description: "Attendee name"
responseStatus:
type: string
description: "Response status"
status:
type: string
description: "Event status"
htmlLink:
type: string
description: "Google Calendar web URL"
created:
type: string
format: date-time
description: "Creation timestamp"
updated:
type: string
format: date-time
description: "Last update timestamp"
recurrence:
type: array
description: "Recurrence rules"
items:
type: string
required: ["id", "summary", "start_time", "end_time", "htmlLink", "status"]
language: python
source:
file: ../python/google_calendar_client.py

View File

@@ -0,0 +1,83 @@
mxcp: 1
tool:
name: get_freebusy
title: Check Calendar Availability
description: |
Check free/busy status across multiple calendars for a specified time range.
Useful for finding meeting slots and checking availability before scheduling.
Example usage:
- "Check my availability tomorrow from 9 AM to 5 PM"
- "Find free time slots across my work and personal calendars"
- "When am I free for a meeting this week?"
- "Check availability for multiple team members' calendars"
tags:
- google-calendar
- freebusy
- availability
annotations:
readOnlyHint: true
idempotentHint: true
parameters:
- name: calendar_ids
type: array
description: "List of calendar IDs to check (use 'primary' for main calendar)"
items:
type: string
description: "Calendar identifier"
minItems: 1
examples: [["primary"], ["primary", "work@company.com"], ["team@company.com", "resources@company.com"]]
- name: time_min
type: string
format: date-time
description: "Start time for availability check (RFC3339 format)"
examples: ["2024-01-15T09:00:00Z", "2024-01-15T09:00:00-08:00"]
- name: time_max
type: string
format: date-time
description: "End time for availability check (RFC3339 format)"
examples: ["2024-01-15T17:00:00Z", "2024-01-15T17:00:00-08:00"]
return:
type: object
description: "Free/busy information for requested calendars"
properties:
time_min:
type: string
format: date-time
description: "Query start time"
time_max:
type: string
format: date-time
description: "Query end time"
calendars:
type: array
description: "Per-calendar availability information"
items:
type: object
properties:
calendar_id:
type: string
description: "Calendar identifier"
busy:
type: array
description: "Busy time periods"
items:
type: object
properties:
start:
type: string
format: date-time
description: "Busy period start"
end:
type: string
format: date-time
description: "Busy period end"
errors:
type: array
description: "API errors for this calendar"
items:
type: object
required: ["time_min", "time_max", "calendars"]
language: python
source:
file: ../python/google_calendar_client.py

View File

@@ -0,0 +1,84 @@
mxcp: 1
tool:
name: list_calendars
title: List Google Calendars
description: |
List all calendars accessible to the authenticated user.
Returns calendars with their access permissions, timezones, and display properties.
Example usage:
- "Show me all my calendars"
- "List calendars I can write to" (with min_access_role: "writer")
- "What calendars do I have access to?"
- "Show me my work calendars" (then filter by name)
tags:
- google-calendar
- calendars
- list
annotations:
readOnlyHint: true
idempotentHint: true
parameters:
- name: show_hidden
type: boolean
description: "Include hidden calendars in results"
default: false
examples: [false, true]
- name: show_deleted
type: boolean
description: "Include deleted calendars in results"
default: false
examples: [false, true]
- name: max_results
type: integer
description: "Maximum number of calendars to return"
default: 100
minimum: 1
maximum: 250
examples: [10, 50, 100]
- name: min_access_role
type: string
description: "Filter by minimum access role"
enum: ["freeBusyReader", "owner", "reader", "writer"]
default: null
examples: ["reader", "writer", "owner"]
return:
type: array
description: "List of accessible calendars"
items:
type: object
properties:
id:
type: string
description: "Calendar identifier (email address or calendar ID)"
summary:
type: string
description: "Calendar name/title"
description:
type: string
description: "Calendar description"
timeZone:
type: string
description: "IANA timezone identifier"
accessRole:
type: string
description: "User's access level"
primary:
type: boolean
description: "Whether this is user's primary calendar"
backgroundColor:
type: string
description: "Background color hex code"
foregroundColor:
type: string
description: "Foreground color hex code"
selected:
type: boolean
description: "Whether calendar is selected in UI"
hidden:
type: boolean
description: "Whether calendar is hidden from list"
required: ["id", "summary", "timeZone", "accessRole"]
language: python
source:
file: ../python/google_calendar_client.py

View File

@@ -0,0 +1,153 @@
mxcp: 1
tool:
name: list_events
title: List Calendar Events
description: |
List events from a specific calendar with optional time filtering and pagination.
Returns events in simplified format optimized for LLM consumption.
Example usage:
- "What's on my calendar today?" (with time_min/time_max for today)
- "Show me this week's meetings" (with time range for current week)
- "List all events in my work calendar" (with specific calendar_id)
- "What meetings do I have next month?" (with future time range)
tags:
- google-calendar
- events
- list
annotations:
readOnlyHint: true
idempotentHint: true
parameters:
- name: calendar_id
type: string
description: "Calendar to query ('primary' or specific calendar ID)"
default: "primary"
examples: ["primary", "work@company.com", "team-calendar@company.com"]
- name: time_min
type: string
format: date-time
description: "Lower bound for event start times (RFC3339 format)"
default: null
examples: ["2024-01-15T00:00:00Z", "2024-01-15T09:00:00-08:00"]
- name: time_max
type: string
format: date-time
description: "Upper bound for event start times (RFC3339 format)"
default: null
examples: ["2024-01-15T23:59:59Z", "2024-01-15T17:00:00-08:00"]
- name: max_results
type: integer
description: "Maximum number of events to return"
default: 250
minimum: 1
maximum: 2500
examples: [10, 50, 250]
- name: single_events
type: boolean
description: "Whether to expand recurring events into instances"
default: true
examples: [true, false]
- name: order_by
type: string
description: "Sort order for events"
enum: ["startTime", "updated"]
default: "startTime"
examples: ["startTime", "updated"]
- name: page_token
type: string
description: "Token for pagination"
default: null
examples: ["CAESGjBpNDd2Nmp2Zml2cXRwYjBpOXA", "next_page_token_example"]
return:
type: object
description: "Event search results with pagination"
properties:
events:
type: array
description: "Matching events"
items:
type: object
description: "Complete event information with simplified time handling"
properties:
id:
type: string
description: "Event identifier"
summary:
type: string
description: "Event title"
description:
type: string
description: "Event description"
location:
type: string
description: "Event location"
start_time:
type: string
format: date-time
description: "Event start time (timezone-aware)"
end_time:
type: string
format: date-time
description: "Event end time (timezone-aware)"
all_day:
type: boolean
description: "Whether this is an all-day event"
time_zone:
type: string
description: "Event timezone (if different from calendar)"
attendees:
type: array
description: "Event attendees"
items:
type: object
creator:
type: object
description: "Event creator"
organizer:
type: object
description: "Event organizer"
status:
type: string
description: "Event status"
htmlLink:
type: string
description: "Google Calendar web URL for this event"
created:
type: string
format: date-time
description: "Event creation timestamp (timezone-aware)"
updated:
type: string
format: date-time
description: "Last update timestamp (timezone-aware)"
recurrence:
type: array
description: "Recurrence rules in RRULE format"
items:
type: string
reminders:
type: object
description: "Reminder settings"
transparency:
type: string
description: "Event transparency"
visibility:
type: string
description: "Event visibility"
calendar_id:
type: string
description: "Calendar containing this event"
etag:
type: string
description: "Event ETag for change detection"
next_page_token:
type: string
description: "Token for pagination"
total_results:
type: integer
description: "Total number of matching events"
required: ["events"]
language: python
source:
file: ../python/google_calendar_client.py

View File

@@ -0,0 +1,97 @@
mxcp: 1
tool:
name: search_events
title: Search Calendar Events
description: |
Search for events matching a text query. Searches across event titles, descriptions,
locations, and attendee information with optional time filtering.
Example usage:
- "Find all meetings with John" (q: "John")
- "Search for events about project Alpha" (q: "project Alpha")
- "Find meetings in the conference room" (q: "conference room")
- "Show me all standup meetings this month" (q: "standup" with time range)
tags:
- google-calendar
- events
- search
annotations:
readOnlyHint: true
idempotentHint: true
parameters:
- name: q
type: string
description: "Free text search query (searches title, description, location, attendees)"
examples: ["John Smith", "project Alpha", "conference room", "standup meeting"]
- name: calendar_id
type: string
description: "Calendar to search ('primary' or specific calendar ID)"
default: "primary"
examples: ["primary", "work@company.com", "team-calendar@company.com"]
- name: time_min
type: string
format: date-time
description: "Earliest event start time to include (RFC3339 format)"
default: null
examples: ["2024-01-15T00:00:00Z", "2024-01-15T09:00:00-08:00"]
- name: time_max
type: string
format: date-time
description: "Latest event start time to include (RFC3339 format)"
default: null
examples: ["2024-01-15T23:59:59Z", "2024-01-15T17:00:00-08:00"]
- name: max_results
type: integer
description: "Maximum number of events to return"
default: 250
minimum: 1
maximum: 2500
examples: [10, 50, 250]
- name: page_token
type: string
description: "Token for pagination"
default: null
examples: ["CAESGjBpNDd2Nmp2Zml2cXRwYjBpOXA", "next_page_token_example"]
return:
type: object
description: "Search results with matching events"
properties:
events:
type: array
description: "Matching events"
items:
type: object
properties:
id:
type: string
description: "Event identifier"
summary:
type: string
description: "Event title"
start_time:
type: string
format: date-time
description: "Event start time"
end_time:
type: string
format: date-time
description: "Event end time"
location:
type: string
description: "Event location"
description:
type: string
description: "Event description"
htmlLink:
type: string
description: "Google Calendar web URL"
next_page_token:
type: string
description: "Token for next page of results"
total_results:
type: integer
description: "Number of results in current page"
required: ["events"]
language: python
source:
file: ../python/google_calendar_client.py

View File

@@ -0,0 +1,53 @@
mxcp: 1
tool:
name: whoami
title: Current User Information
description: |
Get the current authenticated user's information (id, email, name) from Google OAuth context.
Use this tool to verify authentication status and get user profile data.
Example usage:
- "Who am I logged in as?"
- "What's my Google account information?"
- "Show me my user profile"
tags:
- google-calendar
- user
- auth
annotations:
readOnlyHint: true
idempotentHint: true
parameters: []
return:
type: object
description: Current user information from Google OAuth profile
properties:
id:
type: string
description: Google user ID (subject)
email:
type: string
description: User's email address
name:
type: string
description: User's full display name
given_name:
type: string
description: User's first name
family_name:
type: string
description: User's last name
picture:
type: string
description: User's profile picture URL
locale:
type: string
description: User's locale (e.g., 'en-US')
verified_email:
type: boolean
description: Whether email address is verified
required: ["id", "email", "name"]
language: python
source:
file: ../python/google_calendar_client.py
# NOTE: tests section omitted - OAuth tools cannot be tested via mxcp CLI

View File

@@ -0,0 +1,160 @@
# Connect Jira to MXCP with OAuth
This example shows how to connect JIRA to MXCP using secure OAuth authentication.
## What You Get
Once configured, you can query your Jira data directly from MXCP:
```sql
-- Find all issues assigned to you
SELECT jql_query_jira('assignee = currentUser()') AS my_issues;
-- Get recent bugs in a project
SELECT jql_query_jira('project = MYPROJECT AND type = Bug AND created >= -7d') AS recent_bugs;
-- List all your accessible projects
SELECT list_projects_jira() AS projects;
-- Get user information
SELECT get_user_jira('john.doe@company.com') AS user_info;
```
## Quick Setup Guide
### Step 1: Create Your OAuth App in Atlassian
1. Go to [Atlassian Developer Console](https://developer.atlassian.com/console/myapps/)
2. Click **Create****OAuth 2.0 (3LO)**
3. Fill in your app details:
- **App name**: `MXCP Jira Integration` (or whatever you prefer)
- **Description**: `OAuth integration for MXCP`
4. Click **Create**
### Step 2: Configure OAuth Settings
After creating your app:
1. Click on your newly created app
2. Go to **Permissions****Add****Jira API**
3. Add these scopes:
- `read:me` (to read your own profile information)
- `read:jira-work` (to read issues and projects)
- `read:jira-user` (to read user information)
- `offline_access` (to refresh tokens)
4. Go to **Authorization****OAuth 2.0 (3LO)**
5. Add your callback URL based on your deployment:
- **For production**: `https://your-domain.com/atlassian/callback`
- **For local development**: `http://localhost:8000/atlassian/callback`
- **For ngrok testing**: `https://your-ngrok-url.ngrok.io/atlassian/callback`
6. **Important**: Save your **Client ID** and **Client Secret** - you'll need these next!
### Step 3: Set Up Environment Variables
Create a `.env` file or set these environment variables:
```bash
export ATLASSIAN_CLIENT_ID="your-client-id-here"
export ATLASSIAN_CLIENT_SECRET="your-client-secret-here"
```
### Step 4: Configure MXCP
This example includes a ready-to-use `config.yml` file that you can customize with your OAuth credentials. You can either:
- **Use the included file**: Edit the existing `config.yml` in this directory
- **Create your own**: Use the template below
Configuration template:
```yaml
mxcp: 1.0.0
transport:
http:
port: 8000
host: 0.0.0.0
# Set base_url to your server's public URL for production
base_url: http://localhost:8000
projects:
my-jira-project:
profiles:
dev:
# OAuth Configuration
auth:
provider: atlassian
clients:
- client_id: "${ATLASSIAN_CLIENT_ID}"
client_secret: "${ATLASSIAN_CLIENT_SECRET}"
name: "MXCP Jira Integration"
redirect_uris:
# For production, use your actual domain (must match base_url above)
- "https://your-domain.com/atlassian/callback"
# For local development, uncomment the line below:
# - "http://localhost:8000/atlassian/callback"
scopes:
- "mxcp:access"
atlassian:
client_id: "${ATLASSIAN_CLIENT_ID}"
client_secret: "${ATLASSIAN_CLIENT_SECRET}"
scope: "read:me read:jira-work read:jira-user offline_access"
callback_path: "/atlassian/callback"
auth_url: "https://auth.atlassian.com/authorize"
token_url: "https://auth.atlassian.com/oauth/token"
# Plugin Configuration (minimal setup required!)
plugin:
config:
jira_oauth: {} # Named 'jira_oauth' here, but UDFs use 'jira' suffix from mxcp-site.yml
```
### Step 5: Install and Run
1. **Install dependencies**:
```bash
pip install atlassian-python-api requests
```
2. **Start MXCP**:
```bash
# From the examples/jira-oauth directory:
MXCP_CONFIG=config.yml mxcp serve
```
3. **Authenticate**:
- Configure the MXCP server in your MCP client (e.g., Claude Desktop)
- When the client connects, you'll be redirected to Atlassian to authorize the app
- After authorization, you'll be redirected back to your MCP client
- You're now ready to query Jira!
## Available Functions
| Function | Description | Example |
|----------|-------------|---------|
| `jql_query_jira(query, start, limit)` | Execute JQL queries | `SELECT jql_query_jira('project = TEST')` |
| `list_projects_jira()` | List all your accessible projects | `SELECT list_projects_jira()` |
| `get_project_jira(key)` | Get details for a specific project | `SELECT get_project_jira('TEST')` |
| `get_user_jira(username)` | Get user information | `SELECT get_user_jira('john@company.com')` |
## Example Queries
```sql
-- Get your assigned issues
SELECT jql_query_jira('assignee = currentUser() AND status != Done', 0, 20) AS my_open_issues;
-- Find high priority bugs
SELECT jql_query_jira('priority = High AND type = Bug', 0, 10) AS high_priority_bugs;
-- Recent activity in a project
SELECT jql_query_jira('project = MYPROJECT AND updated >= -3d') AS recent_activity;
-- Get project information
SELECT
list_projects_jira() AS all_projects,
get_project_jira('MYPROJECT') AS project_details;
-- Find issues by reporter
SELECT jql_query_jira('reporter = "john.doe@company.com"') AS johns_issues;
```

View File

@@ -0,0 +1,36 @@
mxcp: 1
transport:
http:
port: 8000
host: 0.0.0.0
# Set base_url to your server's public URL for production
base_url: http://localhost:8000
projects:
jira-oauth-demo:
profiles:
dev:
# OAuth Authentication Configuration
auth:
provider: atlassian
clients:
- client_id: "${ATLASSIAN_CLIENT_ID}"
client_secret: "${ATLASSIAN_CLIENT_SECRET}"
name: "MXCP Jira OAuth Integration"
redirect_uris:
# For production, use your actual domain (must match base_url above)
- "http://localhost:8000/atlassian/callback"
scopes:
- "mxcp:access"
atlassian:
client_id: "${ATLASSIAN_CLIENT_ID}"
client_secret: "${ATLASSIAN_CLIENT_SECRET}"
scope: "read:me read:jira-work read:jira-user offline_access"
callback_path: "/atlassian/callback"
auth_url: "https://auth.atlassian.com/authorize"
token_url: "https://auth.atlassian.com/oauth/token"
# Plugin Configuration (minimal configuration - uses OAuth context!)
plugin:
config:
jira_oauth: {} # Named 'jira_oauth' here, but UDFs use 'jira' suffix from mxcp-site.yml

View File

@@ -0,0 +1,8 @@
mxcp: 1
project: jira-oauth-demo
profile: dev
plugin:
- name: jira
module: mxcp_plugin_jira_oauth
config: jira_oauth

View File

@@ -0,0 +1,10 @@
"""
MXCP Jira OAuth Plugin
This plugin provides UDFs for querying Atlassian Jira using OAuth authentication.
Unlike the API token version, this plugin uses OAuth tokens from authenticated users.
"""
from .plugin import MXCPPlugin
__all__ = ["MXCPPlugin"]

View File

@@ -0,0 +1,250 @@
"""
Jira OAuth Plugin Implementation
This module provides UDFs for querying Atlassian Jira using JQL with OAuth 2.0 authentication.
"""
import json
import logging
from typing import Any, Dict, List, Optional
import requests
from atlassian import Jira
from mxcp.plugins import MXCPBasePlugin, udf
from mxcp.sdk.auth.context import get_user_context
logger = logging.getLogger(__name__)
class MXCPPlugin(MXCPBasePlugin):
"""Jira OAuth plugin that provides JQL query functionality using OAuth 2.0 Bearer tokens."""
def __init__(self, config: Dict[str, Any]):
"""Initialize the Jira OAuth plugin.
Args:
config: Plugin configuration containing optional settings
Optional keys:
- oauth_token: Fallback OAuth Bearer token (if not using user context)
"""
super().__init__(config)
self.fallback_oauth_token = config.get("oauth_token", "")
self.instance_url: Optional[str] = None
def _get_oauth_token(self) -> str:
"""Get OAuth token from user context or fallback configuration.
Returns:
OAuth Bearer token
Raises:
ValueError: If no OAuth token is available
"""
# First try to get token from user context (preferred)
user_context = get_user_context()
if user_context and user_context.external_token:
logger.debug("Using OAuth token from user context")
return user_context.external_token
# Fall back to configured token
if self.fallback_oauth_token:
logger.debug("Using fallback OAuth token from configuration")
return self.fallback_oauth_token
raise ValueError("No OAuth token available from user context or configuration")
def _get_cloud_id_and_url(self, oauth_token: str) -> tuple[str, str]:
"""Get the cloud ID and instance URL for the first accessible Jira instance using the OAuth token.
Args:
oauth_token: OAuth Bearer token
Returns:
Tuple of (cloud_id, instance_url) for the first accessible Jira instance
Raises:
ValueError: If cloud ID and URL cannot be retrieved
"""
try:
response = requests.get(
"https://api.atlassian.com/oauth/token/accessible-resources",
headers={"Authorization": f"Bearer {oauth_token}", "Accept": "application/json"},
)
response.raise_for_status()
resources = response.json()
logger.debug(f"Found {len(resources)} accessible resources")
# Use the first accessible resource
if resources:
cloud_id = resources[0].get("id")
instance_url = resources[0].get("url")
logger.info(f"Using cloud ID: {cloud_id} for instance: {instance_url}")
return cloud_id, instance_url
raise ValueError(f"No accessible resources found for OAuth token")
except requests.RequestException as e:
logger.error(f"Failed to get cloud ID and URL: {e}")
raise ValueError(f"Failed to retrieve cloud ID and URL: {e}")
def _create_jira_client(self) -> Jira:
"""Create a Jira client with OAuth authentication using the correct API gateway URL.
Returns:
Configured Jira client instance
"""
oauth_token = self._get_oauth_token()
# Get the cloud ID and instance URL for the first accessible Jira instance
cloud_id, instance_url = self._get_cloud_id_and_url(oauth_token)
# Store the instance URL for constructing web UI URLs
self.instance_url = instance_url
# Construct the API gateway URL for OAuth requests
api_gateway_url = f"https://api.atlassian.com/ex/jira/{cloud_id}"
logger.info("API Gateway URL: %s", api_gateway_url)
# Create a requests session with OAuth Bearer token
session = requests.Session()
session.headers["Authorization"] = f"Bearer {oauth_token}"
# Create and return Jira client with the OAuth session and API gateway URL
# Explicitly set cloud=True since we're using Jira Cloud with OAuth
return Jira(url=api_gateway_url, session=session, cloud=True)
@udf
def jql_query(self, query: str, start: Optional[int] = 0, limit: Optional[int] = None) -> str:
"""Execute a JQL query against Jira using OAuth authentication.
Args:
query: The JQL query string
start: Starting index for pagination (default: 0)
limit: Maximum number of results to return (default: None, meaning no limit)
Returns:
JSON string containing Jira issues matching the query
"""
logger.info(
"Executing JQL query with OAuth: %s with start=%s, limit=%s", query, start, limit
)
# Create Jira client with current user's OAuth token
jira = self._create_jira_client()
raw = jira.jql(
jql=query,
start=start,
limit=limit,
fields=(
"key,summary,status,resolution,resolutiondate,"
"assignee,reporter,issuetype,priority,"
"created,updated,labels,fixVersions,parent"
),
)
def _name(obj: Optional[Dict[str, Any]]) -> Optional[str]:
"""Return obj['name'] if present, else None."""
return obj.get("name") if obj else None
def _key(obj: Optional[Dict[str, Any]]) -> Optional[str]:
return obj.get("key") if obj else None
cleaned: List[Dict[str, Any]] = []
for issue in raw.get("issues", []):
f = issue["fields"]
cleaned.append(
{
"key": issue["key"],
"summary": f.get("summary"),
"status": _name(f.get("status")),
"resolution": _name(f.get("resolution")),
"resolution_date": f.get("resolutiondate"),
"assignee": _name(f.get("assignee")),
"reporter": _name(f.get("reporter")),
"type": _name(f.get("issuetype")),
"priority": _name(f.get("priority")),
"created": f.get("created"),
"updated": f.get("updated"),
"labels": f.get("labels") or [],
"fix_versions": [_name(v) for v in f.get("fixVersions", [])],
"parent": _key(f.get("parent")),
"url": f"{self.instance_url}/browse/{issue['key']}", # web UI URL
}
)
return json.dumps(cleaned)
@udf
def get_user(self, username: str) -> str:
"""Get details for a specific user by username using OAuth.
Args:
username: The username to search for
Returns:
JSON string containing the user details
"""
logger.info("Getting user details with OAuth for username: %s", username)
# Create Jira client with current user's OAuth token
jira = self._create_jira_client()
return json.dumps(jira.user_find_by_user_string(query=username))
@udf
def list_projects(self) -> str:
"""List all accessible Jira projects using OAuth authentication.
Returns:
JSON string containing an array of accessible Jira projects
"""
logger.info("Listing all projects with OAuth")
# Create Jira client with current user's OAuth token
jira = self._create_jira_client()
raw_projects: List[Dict[str, Any]] = jira.projects()
def safe_name(obj: Optional[Dict[str, Any]]) -> Optional[str]:
return obj.get("displayName") or obj.get("name") if obj else None
concise: List[Dict[str, Any]] = []
for p in raw_projects:
concise.append(
{
"key": p.get("key"),
"name": p.get("name"),
"type": p.get("projectTypeKey"), # e.g. software, business
"lead": safe_name(p.get("lead")),
"url": f"{self.instance_url}/projects/{p.get('key')}", # web UI URL
}
)
return json.dumps(concise)
@udf
def get_project(self, project_key: str) -> str:
"""Get details for a specific project by its key using OAuth.
Args:
project_key: The project key (e.g., 'TEST' for project TEST)
Returns:
JSON string containing the project details
"""
logger.info("Getting project details with OAuth for key: %s", project_key)
# Create Jira client with current user's OAuth token
jira = self._create_jira_client()
info = jira.project(project_key)
# remove the self key if it exists
if "self" in info:
info.pop("self")
# Add web UI URL
info["url"] = f"{self.instance_url}/projects/{project_key}"
return json.dumps(info)

View File

@@ -0,0 +1,2 @@
-- Get the username of the currently authenticated user
SELECT get_username() as authenticated_user;

View File

@@ -0,0 +1,2 @@
-- Get details for a specific Jira project using OAuth authentication
SELECT get_project_jira($project_key) as result;

View File

@@ -0,0 +1,2 @@
-- Get details for a specific Jira user using OAuth authentication
SELECT get_user_jira($username) as result;

View File

@@ -0,0 +1,2 @@
-- Example JQL query endpoint using OAuth authentication
SELECT jql_query_jira($query, $start, $limit) as result;

View File

@@ -0,0 +1,2 @@
-- List all projects in Jira using OAuth authentication
SELECT list_projects_jira() as result;

View File

@@ -0,0 +1,25 @@
mxcp: 1
tool:
name: get_current_user
description: |
Get the username of the currently authenticated user in MXCP.
This tool returns the username of the person who is authenticated via OAuth with Jira.
It's useful for understanding whose credentials are being used for Jira API calls,
and can help verify that the OAuth authentication flow completed successfully.
The username typically corresponds to the Atlassian account email address.
type: tool
annotations:
title: Get Current Authenticated User
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: false
return:
type: string
description: |
The username (typically email address) of the currently authenticated user.
Returns NULL if no user is authenticated.
language: "sql"
source:
file: "../sql/get_current_user.sql"

View File

@@ -0,0 +1,32 @@
mxcp: 1
tool:
name: get_project
description: |
Get details for a specific project in your Jira instance by its project key using OAuth authentication.
Returns a JSON string containing the project's details.
type: tool
annotations:
title: Get Project Details (OAuth)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: project_key
type: string
description: |
The project key to search for. This is the short identifier for the project (e.g., 'TEST' for project TEST).
Project keys are typically uppercase and contain only letters and numbers.
examples: [
"TEST",
"PROJ",
"DEV"
]
return:
type: string
description: |
A JSON string containing the project's details.
language: "sql"
source:
file: "../sql/get_project.sql"

View File

@@ -0,0 +1,30 @@
mxcp: 1
tool:
name: get_user
description: |
Get details for a specific user in your Jira instance by their username using OAuth authentication.
Returns a JSON string containing the user's details.
type: tool
annotations:
title: Get User Details (OAuth)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: username
type: string
description: |
The username to search for. This is typically the user's email address or username in Jira.
examples: [
"john.doe@example.com",
"jane.smith"
]
return:
type: string
description: |
A JSON string containing the user's details.
language: "sql"
source:
file: "../sql/get_user.sql"

View File

@@ -0,0 +1,50 @@
mxcp: 1
tool:
name: jql
description: |
Execute a JQL (Jira Query Language) query to search for issues in your Jira instance using OAuth authentication.
Returns a JSON string containing the matching issues with their details.
Use the start and limit parameters to paginate through large result sets.
type: tool
annotations:
title: JQL Query (OAuth)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
parameters:
- name: query
type: string
description: |
The JQL query string to execute. Examples:
- "project = TEST" to find all issues in the TEST project
- "assignee = currentUser()" to find issues assigned to you
- "status = 'In Progress'" to find issues in progress
examples: [
"project = TEST",
"status = 'In Progress'",
"project = TEST AND status = 'Done'",
"created >= -30d ORDER BY created DESC"
]
- name: start
type: integer
description: |
The index of the first result to return (0-based).
Use this for pagination: start=0 for first page, start=50 for second page, etc.
Defaults to 0 if not specified.
examples: [0, 50, 100]
- name: limit
type: integer
description: |
Maximum number of results to return.
If not specified, returns all matching results.
Recommended to use with start parameter for pagination.
examples: [50, 100, 200]
return:
type: string
description: |
A JSON string containing an array of Jira issues.
language: "sql"
source:
file: "../sql/jql.sql"

View File

@@ -0,0 +1,21 @@
mxcp: 1
tool:
name: list_projects
description: |
List all projects in your Jira instance using OAuth authentication.
Returns a JSON string containing an array of projects with their details.
type: tool
annotations:
title: List Projects (OAuth)
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
return:
type: string
description: |
A JSON string containing an array of Jira projects.
language: "sql"
source:
file: "../sql/list_projects.sql"

View File

@@ -0,0 +1,145 @@
# MXCP Jira Python Endpoints Example
This example demonstrates how to use MXCP with Jira data using Python endpoints. This approach uses Python functions directly as MCP tools.
## Overview
This example provides Python MCP endpoints that allow you to:
- Execute JQL queries to search issues
- Get detailed information for specific issues
- Get user information
- List projects and their details
- Get project metadata
## Implementation Approach
This example uses Python functions that are exposed as MCP tools:
- Python functions handle the Jira API interactions
- Tool definitions map to these Python functions
- Results are returned as JSON data
## Configuration
### 1. Creating an Atlassian API Token
**Important:** This plugin currently only supports API tokens **without scopes**. While Atlassian has introduced scoped API tokens, there are known compatibility issues when using scoped tokens with basic authentication that this plugin relies on.
To create an API token without scopes:
1. **Log in to your Atlassian account** at [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens)
2. **Verify your identity** (if prompted):
- Atlassian may ask you to verify your identity before creating API tokens
- Check your email for a one-time passcode and enter it when prompted
3. **Create the API token**:
- Click **"Create API token"** (not "Create API token with scopes")
- Enter a descriptive name for your token (e.g., "MXCP Jira Python Integration")
- Select an expiration date (tokens can last from 1 day to 1 year)
- Click **"Create"**
4. **Copy and save your token**:
- Click **"Copy to clipboard"** to copy the token
- **Important:** Save this token securely (like in a password manager) as you won't be able to view it again
- This token will be used as your "password" in the configuration below
### 2. User Configuration
Add the following to your MXCP user config (`~/.mxcp/config.yml`):
```yaml
mxcp: 1
projects:
jira-demo:
profiles:
default:
secrets:
- name: "jira"
type: "python"
parameters:
url: "https://your-domain.atlassian.net"
username: "your-email@example.com"
password: "your-api-token" # Use the API token you created above
```
### 3. Site Configuration
Create an `mxcp-site.yml` file:
```yaml
mxcp: 1
project: jira-demo
profile: default
secrets:
- jira
```
## Available Tools
### JQL Query
Execute JQL queries:
```bash
mxcp run tool jql_query --param query="project = TEST" --param limit=10
```
### Get Issue
Get detailed information for a specific issue by its key:
```bash
mxcp run tool get_issue --param issue_key="RD-123"
```
### Get User
Get a specific user by their account ID:
```bash
mxcp run tool get_user --param account_id="557058:ab168c94-8485-405c-88e6-6458375eb30b"
```
### Search Users
Search for users by name, email, or other criteria:
```bash
mxcp run tool search_user --param query="john.doe@example.com"
```
### List Projects
List all projects:
```bash
mxcp run tool list_projects
```
### Get Project
Get project details:
```bash
mxcp run tool get_project --param project_key="TEST"
```
### Get Project Roles
Get all roles available in a project:
```bash
mxcp run tool get_project_roles --param project_key="TEST"
```
### Get Project Role Users
Get users and groups for a specific role in a project:
```bash
mxcp run tool get_project_role_users --param project_key="TEST" --param role_name="Developers"
```
## Project Structure
```
jira-python/
├── mxcp-site.yml # Site configuration
├── python/ # Python implementations
│ └── jira_endpoints.py # All JIRA endpoint functions
├── tools/ # Tool definitions
│ ├── jql_query.yml
│ ├── get_issue.yml
│ ├── get_user.yml
│ ├── search_user.yml
│ ├── list_projects.yml
│ ├── get_project.yml
│ ├── get_project_roles.yml
│ └── get_project_role_users.yml
└── README.md
```

View File

@@ -0,0 +1,17 @@
mxcp: 1
# Sample configuration file for JIRA Python endpoints example
# Copy this to ~/.mxcp/config.yml and update with your JIRA details
projects:
jira-demo:
profiles:
default:
secrets:
- name: "jira"
type: "python"
parameters:
url: "${JIRA_URL}"
username: "${JIRA_USERNAME}"
password: "${JIRA_API_TOKEN}"

View File

@@ -0,0 +1,5 @@
mxcp: 1
project: jira-demo
profile: default
secrets:
- jira

View File

@@ -0,0 +1,569 @@
"""
JIRA Python Endpoints
This module provides direct Python MCP endpoints for querying Atlassian JIRA.
This is a simpler alternative to the plugin-based approach.
"""
from typing import Dict, Any, List, Optional, Callable
import logging
from atlassian import Jira
from mxcp.runtime import config, on_init, on_shutdown
import threading
import functools
import time
logger = logging.getLogger(__name__)
# Global JIRA client for reuse across all function calls
jira_client: Optional[Jira] = None
# Thread lock to protect client initialization
_client_lock = threading.Lock()
@on_init
def setup_jira_client() -> None:
"""Initialize JIRA client when server starts.
Thread-safe: multiple threads can safely call this simultaneously.
"""
global jira_client
with _client_lock:
logger.info("Initializing JIRA client...")
jira_config = config.get_secret("jira")
if not jira_config:
raise ValueError(
"JIRA configuration not found. Please configure JIRA secrets in your user config."
)
required_keys = ["url", "username", "password"]
missing_keys = [key for key in required_keys if not jira_config.get(key)]
if missing_keys:
raise ValueError(f"Missing JIRA configuration keys: {', '.join(missing_keys)}")
jira_client = Jira(
url=jira_config["url"],
username=jira_config["username"],
password=jira_config["password"],
cloud=True,
)
logger.info("JIRA client initialized successfully")
@on_shutdown
def cleanup_jira_client() -> None:
"""Clean up JIRA client when server stops."""
global jira_client
if jira_client:
# JIRA client doesn't need explicit cleanup, but we'll clear the reference
jira_client = None
logger.info("JIRA client cleaned up")
def retry_on_session_expiration(func: Callable[..., Any]) -> Callable[..., Any]:
"""
Decorator that automatically retries functions on JIRA session expiration.
This only retries on HTTP 401 Unauthorized errors, not other authentication failures.
Retries up to 2 times on session expiration (3 total attempts).
Thread-safe: setup_jira_client() handles concurrent access internally.
Usage:
@retry_on_session_expiration
def my_jira_function():
# Function that might fail due to session expiration
pass
"""
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
max_retries = 2 # Hardcoded: 2 retries = 3 total attempts
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except Exception as e:
# Check if this is a 401 Unauthorized error (session expired)
if _is_session_expired(e):
if attempt < max_retries:
logger.warning(
f"Session expired on attempt {attempt + 1} in {func.__name__}: {e}"
)
logger.info(
f"Retrying after re-initializing client (attempt {attempt + 2}/{max_retries + 1})"
)
try:
setup_jira_client() # Thread-safe internally
time.sleep(0.1) # Small delay to avoid immediate retry
except Exception as setup_error:
logger.error(f"Failed to re-initialize JIRA client: {setup_error}")
raise setup_error # Raise the setup error, not the original session error
else:
# Last attempt failed, re-raise the session expiration error
raise e
else:
# Not a session expiration error, re-raise immediately
raise e
return wrapper
def _is_session_expired(exception: Exception) -> bool:
"""Check if the exception indicates a JIRA session has expired."""
error_msg = str(exception).lower()
# Check for HTTP 401 Unauthorized
if "401" in error_msg or "unauthorized" in error_msg:
return True
# Check for common session expiration messages
if any(
phrase in error_msg
for phrase in [
"session expired",
"session invalid",
"authentication failed",
"invalid session",
"session timeout",
]
):
return True
return False
def _get_jira_client() -> Jira:
"""Get the global JIRA client."""
if jira_client is None:
raise RuntimeError("JIRA client not initialized. Make sure the server is started properly.")
return jira_client
@retry_on_session_expiration
def jql_query(
query: str, start: Optional[int] = None, limit: Optional[int] = None
) -> List[Dict[str, Any]]:
"""Execute a JQL query against Jira.
Args:
query: The JQL query string
start: Starting index for pagination (default: None, which becomes 0)
limit: Maximum number of results to return (default: None, meaning no limit)
Returns:
List of Jira issues matching the query
"""
logger.info("Executing JQL query: %s with start=%s, limit=%s", query, start, limit)
jira = _get_jira_client()
raw = jira.jql(
jql=query,
start=start if start is not None else 0,
limit=limit,
fields=(
"key,summary,status,resolution,resolutiondate,"
"assignee,reporter,issuetype,priority,"
"created,updated,labels,fixVersions,parent"
),
)
if not raw:
raise ValueError("JIRA JQL query returned empty result")
def _name(obj: Optional[Dict[str, Any]]) -> Optional[str]:
"""Return obj['name'] if present, else None."""
return obj.get("name") if obj else None
def _key(obj: Optional[Dict[str, Any]]) -> Optional[str]:
return obj.get("key") if obj else None
cleaned: List[Dict[str, Any]] = []
jira_url = jira.url
for issue in raw.get("issues", []):
f = issue["fields"]
cleaned.append(
{
"key": issue["key"],
"summary": f.get("summary"),
"status": _name(f.get("status")),
"resolution": _name(f.get("resolution")),
"resolution_date": f.get("resolutiondate"),
"assignee": _name(f.get("assignee")),
"reporter": _name(f.get("reporter")),
"type": _name(f.get("issuetype")),
"priority": _name(f.get("priority")),
"created": f.get("created"),
"updated": f.get("updated"),
"labels": f.get("labels") or [],
"fix_versions": [_name(v) for v in f.get("fixVersions", [])],
"parent": _key(f.get("parent")),
"url": f"{jira_url}/browse/{issue['key']}", # web UI URL
}
)
return cleaned
@retry_on_session_expiration
def get_issue(issue_key: str) -> Dict[str, Any]:
"""Get detailed information for a specific JIRA issue by its key.
Args:
issue_key: The issue key (e.g., 'RD-123', 'TEST-456')
Returns:
Dictionary containing comprehensive issue information
Raises:
ValueError: If issue is not found or access is denied
"""
logger.info("Getting issue details for key: %s", issue_key)
jira = _get_jira_client()
# Get issue by key - this method handles the REST API call
issue = jira.issue(issue_key)
# Extract and clean up the most important fields for easier consumption
fields = issue.get("fields", {})
jira_url = jira.url
def _safe_get(obj: Any, key: str, default: Any = None) -> Any:
"""Safely get a value from a dict/object that might be None."""
if obj is None:
return default
if isinstance(obj, dict):
return obj.get(key, default)
return getattr(obj, key, default)
cleaned_issue = {
"key": issue.get("key"),
"id": issue.get("id"),
"summary": fields.get("summary"),
"description": fields.get("description"),
"status": _safe_get(fields.get("status"), "name"),
"assignee": _safe_get(fields.get("assignee"), "displayName"),
"assignee_account_id": _safe_get(fields.get("assignee"), "accountId"),
"reporter": _safe_get(fields.get("reporter"), "displayName"),
"reporter_account_id": _safe_get(fields.get("reporter"), "accountId"),
"issue_type": _safe_get(fields.get("issuetype"), "name"),
"priority": _safe_get(fields.get("priority"), "name"),
"resolution": _safe_get(fields.get("resolution"), "name"),
"resolution_date": fields.get("resolutiondate"),
"created": fields.get("created"),
"updated": fields.get("updated"),
"due_date": fields.get("duedate"),
"labels": fields.get("labels", []) or [],
"components": (
[comp.get("name") for comp in fields.get("components", []) if comp and comp.get("name")]
if fields.get("components")
else []
),
"fix_versions": (
[ver.get("name") for ver in fields.get("fixVersions", []) if ver and ver.get("name")]
if fields.get("fixVersions")
else []
),
"project": {
"key": _safe_get(fields.get("project"), "key"),
"name": _safe_get(fields.get("project"), "name"),
},
"parent": _safe_get(fields.get("parent"), "key"),
"url": f"{jira_url}/browse/{issue.get('key')}",
}
return cleaned_issue
@retry_on_session_expiration
def get_user(account_id: str) -> Dict[str, Any]:
"""Get a specific user by their unique account ID.
Args:
account_id: The unique Atlassian account ID for the user.
Example: "557058:ab168c94-8485-405c-88e6-6458375eb30b"
Returns:
Dictionary containing filtered user details
Raises:
ValueError: If user is not found or account ID is invalid
"""
logger.info("Getting user details for account ID: %s", account_id)
jira = _get_jira_client()
# Get user by account ID - pass as account_id parameter for Jira Cloud
user = jira.user(account_id=account_id)
# Return only the requested fields
return {
"accountId": user.get("accountId"),
"displayName": user.get("displayName"),
"emailAddress": user.get("emailAddress"),
"active": user.get("active"),
"timeZone": user.get("timeZone"),
}
@retry_on_session_expiration
def search_user(query: str) -> List[Dict[str, Any]]:
"""Search for users by query string (username, email, or display name).
Args:
query: Search term - can be username, email, display name, or partial matches.
Examples: "ben@raw-labs.com", "Benjamin Gaidioz", "ben", "benjamin", "gaidioz"
Returns:
List of matching users with filtered fields. Empty list if no matches found.
"""
logger.info("Searching for users with query: %s", query)
jira = _get_jira_client()
# user_find_by_user_string returns a list of users matching the query
users = jira.user_find_by_user_string(query=query)
if not users:
return []
# Filter users to only include relevant fields
filtered_users = []
for user in users:
filtered_users.append(
{
"accountId": user.get("accountId"),
"displayName": user.get("displayName"),
"emailAddress": user.get("emailAddress"),
"active": user.get("active"),
"timeZone": user.get("timeZone"),
}
)
return filtered_users
@retry_on_session_expiration
def list_projects() -> List[Dict[str, Any]]:
"""Return a concise list of Jira projects.
Returns:
List of dictionaries containing project information
"""
logger.info("Listing all projects")
jira = _get_jira_client()
raw_projects: List[Dict[str, Any]] = jira.projects(expand="lead")
def safe_name(obj: Optional[Dict[str, Any]]) -> Optional[str]:
return obj.get("displayName") or obj.get("name") if obj else None
concise: List[Dict[str, Any]] = []
jira_url = jira.url
for p in raw_projects:
concise.append(
{
"key": p.get("key"),
"name": p.get("name"),
"type": p.get("projectTypeKey"), # e.g. software, business
"lead": safe_name(p.get("lead")),
"url": f"{jira_url}/projects/{p.get('key')}", # web UI URL
}
)
return concise
@retry_on_session_expiration
def get_project(project_key: str) -> Dict[str, Any]:
"""Get details for a specific project by its key.
Args:
project_key: The project key (e.g., 'TEST' for project TEST)
Returns:
Dictionary containing the project details
Raises:
ValueError: If project is not found or access is denied
"""
logger.info("Getting project details for key: %s", project_key)
jira = _get_jira_client()
try:
info = jira.project(project_key)
except Exception as e:
# Handle various possible errors from the JIRA API
error_msg = str(e).lower()
if "404" in error_msg or "not found" in error_msg:
raise ValueError(f"Project '{project_key}' not found in JIRA")
elif "403" in error_msg or "forbidden" in error_msg:
raise ValueError(f"Access denied to project '{project_key}' in JIRA")
else:
# Re-raise other errors with context
raise ValueError(f"Error retrieving project '{project_key}': {e}") from e
# Filter to essential fields only to avoid response size issues
cleaned_info = {
"key": info.get("key"),
"name": info.get("name"),
"description": info.get("description"),
"projectTypeKey": info.get("projectTypeKey"),
"simplified": info.get("simplified"),
"style": info.get("style"),
"isPrivate": info.get("isPrivate"),
"archived": info.get("archived"),
}
# Add lead info if present
if "lead" in info and info["lead"]:
cleaned_info["lead"] = {
"displayName": info["lead"].get("displayName"),
"emailAddress": info["lead"].get("emailAddress"),
"accountId": info["lead"].get("accountId"),
"active": info["lead"].get("active"),
}
cleaned_info["url"] = f"{jira.url}/projects/{project_key}"
return cleaned_info
@retry_on_session_expiration
def get_project_roles(project_key: str) -> List[Dict[str, Any]]:
"""Get all roles available in a project.
Args:
project_key: The project key (e.g., 'TEST' for project TEST)
Returns:
List of roles available in the project
Raises:
ValueError: If project is not found or access is denied
"""
logger.info("Getting project roles for key: %s", project_key)
jira = _get_jira_client()
try:
# Get all project roles using the correct method
project_roles = jira.get_project_roles(project_key)
result = []
for role_name, role_url in project_roles.items():
# Extract role ID from URL (e.g., "https://domain.atlassian.net/rest/api/3/project/10000/role/10002")
role_id = role_url.split("/")[-1]
result.append({"name": role_name, "id": role_id})
return result
except Exception as e:
# Handle various possible errors from the JIRA API
error_msg = str(e).lower()
if "404" in error_msg or "not found" in error_msg:
raise ValueError(f"Project '{project_key}' not found in JIRA")
elif "403" in error_msg or "forbidden" in error_msg:
raise ValueError(f"Access denied to project '{project_key}' in JIRA")
else:
# Re-raise other errors with context
raise ValueError(f"Error retrieving project roles for '{project_key}': {e}") from e
@retry_on_session_expiration
def get_project_role_users(project_key: str, role_name: str) -> Dict[str, Any]:
"""Get users and groups for a specific role in a project.
Args:
project_key: The project key (e.g., 'TEST' for project TEST)
role_name: The name of the role to get users for
Returns:
Dictionary containing users and groups for the specified role
Raises:
ValueError: If project or role is not found, or access is denied
"""
logger.info("Getting users for role '%s' in project '%s'", role_name, project_key)
jira = _get_jira_client()
try:
# First get all project roles to find the role ID
project_roles = jira.get_project_roles(project_key)
if role_name not in project_roles:
available_roles = list(project_roles.keys())
raise ValueError(
f"Role '{role_name}' not found in project '{project_key}'. Available roles: {available_roles}"
)
# Extract role ID from URL
role_url = project_roles[role_name]
role_id = role_url.split("/")[-1]
# Get role details including actors (users and groups)
role_details = jira.get_project_actors_for_role_project(project_key, role_id)
result = {
"project_key": project_key,
"role_name": role_name,
"role_id": role_id,
"users": [],
"groups": [],
}
# Process actors (role_details is a list of actors)
if isinstance(role_details, list):
for actor in role_details:
if isinstance(actor, dict):
actor_type = actor.get("type", "")
if actor_type == "atlassian-user-role-actor":
# Individual user
user_info = {
"accountId": actor.get("actorUser", {}).get("accountId"),
"displayName": actor.get("displayName"),
}
result["users"].append(user_info)
elif actor_type == "atlassian-group-role-actor":
# Group
group_info = {
"name": actor.get("displayName"),
"groupId": actor.get("actorGroup", {}).get("groupId"),
}
result["groups"].append(group_info)
else:
# Handle other actor types or simple user entries
display_name = actor.get("displayName") or actor.get("name")
if display_name:
user_info = {
"accountId": actor.get("accountId"),
"displayName": display_name,
}
result["users"].append(user_info)
return result
except ValueError:
# Re-raise ValueError as-is (these are our custom error messages)
raise
except Exception as e:
# Handle various possible errors from the JIRA API
error_msg = str(e).lower()
# Don't handle 401 errors here - let the retry decorator handle them
if "401" in error_msg or "unauthorized" in error_msg:
raise e # Let the retry decorator catch this
elif "404" in error_msg or "not found" in error_msg:
raise ValueError(f"Project '{project_key}' not found in JIRA")
elif "403" in error_msg or "forbidden" in error_msg:
raise ValueError(f"Access denied to project '{project_key}' in JIRA")
else:
# Re-raise other errors with context
raise ValueError(
f"Error retrieving users for role '{role_name}' in project '{project_key}': {e}"
) from e

View File

@@ -0,0 +1,114 @@
mxcp: 1
tool:
name: get_issue
description: |
Get detailed information for a specific JIRA issue by its key.
Returns comprehensive issue information including all fields, assignee, reporter, etc.
type: tool
annotations:
title: Get Issue
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: issue_key
type: string
description: |
The issue key (e.g., 'RD-123', 'TEST-456').
This is the unique identifier for the issue visible in the Jira UI.
examples: [
"RD-123",
"TEST-456",
"PROJ-789"
]
return:
type: object
properties:
key:
type: string
description: The issue key
id:
type: string
description: The issue ID
summary:
type: string
description: The issue summary
description:
type: string
description: The issue description
status:
type: string
description: The current status
assignee:
type: string
description: The assignee display name
assignee_account_id:
type: string
description: The assignee account ID
reporter:
type: string
description: The reporter display name
reporter_account_id:
type: string
description: The reporter account ID
issue_type:
type: string
description: The issue type
priority:
type: string
description: The priority level
resolution:
type: string
description: The resolution
resolution_date:
type: string
description: The resolution date
created:
type: string
description: The creation date
updated:
type: string
description: The last update date
due_date:
type: string
description: The due date
labels:
type: array
items:
type: string
description: The issue labels
components:
type: array
items:
type: string
description: The issue components
fix_versions:
type: array
items:
type: string
description: The fix versions
project:
type: object
properties:
key:
type: string
name:
type: string
description: The project information
parent:
type: string
description: The parent issue key
url:
type: string
description: The issue URL
tests:
- name: "Get issue by key"
description: "Verify issue retrieval returns expected structure"
arguments:
- key: issue_key
value: "RD-15333"

View File

@@ -0,0 +1,76 @@
mxcp: 1
tool:
name: get_project
description: |
Get details for a specific project by its key.
Returns comprehensive project information including description, settings, and lead.
type: tool
annotations:
title: Get Project
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: project_key
type: string
description: |
The project key (e.g., 'TEST' for project TEST).
This is the short identifier for the project.
examples: [
"TEST",
"PROJ",
"DEV"
]
return:
type: object
properties:
key:
type: string
description: The project key
name:
type: string
description: The project name
description:
type: string
description: The project description
projectTypeKey:
type: string
description: The project type key
simplified:
type: boolean
description: Whether the project is simplified
style:
type: string
description: The project style
isPrivate:
type: boolean
description: Whether the project is private
archived:
type: boolean
description: Whether the project is archived
lead:
type: object
properties:
displayName:
type: string
emailAddress:
type: string
accountId:
type: string
active:
type: boolean
description: The project lead information
url:
type: string
description: The project URL
tests:
- name: "Get project by key"
description: "Verify project retrieval returns expected structure"
arguments:
- key: project_key
value: "RD"

View File

@@ -0,0 +1,78 @@
mxcp: 1
tool:
name: get_project_role_users
description: |
Get users and groups for a specific role in a project.
Returns detailed information about users and groups assigned to the role.
type: tool
annotations:
title: Get Project Role Users
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: project_key
type: string
description: |
The project key (e.g., 'TEST' for project TEST).
This is the short identifier for the project.
examples: [
"TEST",
"PROJ",
"DEV"
]
- name: role_name
type: string
description: |
The name of the role to get users for.
Common roles include 'Administrators', 'Developers', 'Users'.
examples: [
"Administrators",
"Developers",
"Users"
]
return:
type: object
properties:
project_key:
type: string
description: The project key
role_name:
type: string
description: The role name
role_id:
type: string
description: The role ID
users:
type: array
items:
type: object
properties:
accountId:
type: string
displayName:
type: string
description: List of users in the role
groups:
type: array
items:
type: object
properties:
name:
type: string
groupId:
type: string
description: List of groups in the role
tests:
- name: "Get role users"
description: "Verify role users returns expected structure"
arguments:
- key: project_key
value: "RD"
- key: role_name
value: "Administrators"

View File

@@ -0,0 +1,45 @@
mxcp: 1
tool:
name: get_project_roles
description: |
Get all roles available in a project.
Returns a list of roles with their IDs and URLs.
type: tool
annotations:
title: Get Project Roles
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: project_key
type: string
description: |
The project key (e.g., 'TEST' for project TEST).
This is the short identifier for the project.
examples: [
"TEST",
"PROJ",
"DEV"
]
return:
type: array
items:
type: object
properties:
name:
type: string
description: The role name
id:
type: string
description: The role ID
tests:
- name: "Get project roles"
description: "Verify project roles returns array of roles"
arguments:
- key: project_key
value: "RD"

View File

@@ -0,0 +1,53 @@
mxcp: 1
tool:
name: get_user
description: |
Get a specific user by their unique account ID.
Returns detailed user information including display name, email, and account status.
type: tool
annotations:
title: Get User
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: account_id
type: string
description: |
The unique Atlassian account ID for the user.
This is typically in the format: "557058:ab168c94-8485-405c-88e6-6458375eb30b"
You can get account IDs from other API calls like get_issue or search_user.
examples: [
"557058:ab168c94-8485-405c-88e6-6458375eb30b",
"5b10ac8d82e05b22cc7d4ef5",
"712020:0e99e8b3-7b3a-4b7c-9a1f-9e5d8c7b4a3e"
]
return:
type: object
properties:
accountId:
type: string
description: The account ID
displayName:
type: string
description: The display name
emailAddress:
type: string
description: The email address
active:
type: boolean
description: Whether the user is active
timeZone:
type: string
description: The user's time zone
tests:
- name: "Get user by account ID"
description: "Just run the tool"
arguments:
- key: account_id
value: "557058:ab168c94-8485-405c-88e6-6458375eb30b"

View File

@@ -0,0 +1,84 @@
mxcp: 1
tool:
name: jql_query
description: |
Execute a JQL (Jira Query Language) query to search for issues in your Jira instance.
Returns a list of issues with their details.
Use the start and limit parameters to paginate through large result sets.
type: tool
annotations:
title: JQL Query
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: query
type: string
description: |
The JQL query string to execute. Examples:
- "project = TEST" to find all issues in the TEST project
- "assignee = currentUser()" to find issues assigned to you
- "status = 'In Progress'" to find issues in progress
examples: [
"project = TEST",
"status = 'In Progress'",
"project = TEST AND status = 'Done'",
"created >= -30d ORDER BY created DESC"
]
- name: start
type: integer
description: |
The index of the first result to return (0-based).
Use this for pagination: start=0 for first page, start=50 for second page, etc.
Defaults to 0 if not specified.
default: 0
examples: [0, 50, 100]
- name: limit
type: integer
description: |
Maximum number of results to return.
If not specified, returns all matching results.
Recommended to use with start parameter for pagination.
examples: [50, 100, 200]
default: null
return:
type: array
items:
type: object
properties:
key:
type: string
summary:
type: string
status:
type: string
assignee:
type: string
reporter:
type: string
created:
type: string
updated:
type: string
url:
type: string
tests:
- name: "Basic project query"
description: "Verify JQL query returns array of issues"
arguments:
- key: query
value: "project = RD"
- key: limit
value: 1
- name: "Status filter query"
description: "Verify JQL query with status filter"
arguments:
- key: query
value: "status = 'In Progress'"
- key: limit
value: 1

View File

@@ -0,0 +1,42 @@
mxcp: 1
tool:
name: list_projects
description: |
Return a concise list of Jira projects.
Returns basic project information including key, name, type, and lead.
type: tool
annotations:
title: List Projects
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters: []
return:
type: array
items:
type: object
properties:
key:
type: string
description: The project key
name:
type: string
description: The project name
type:
type: string
description: The project type
lead:
type: string
description: The project lead
url:
type: string
description: The project URL
tests:
- name: "List all projects"
description: "Verify projects list returns array of projects"
arguments: []

View File

@@ -0,0 +1,56 @@
mxcp: 1
tool:
name: search_user
description: |
Search for users by query string (username, email, or display name).
Returns a list of matching users with their details.
type: tool
annotations:
title: Search User
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
language: python
source:
file: ../python/jira_endpoints.py
parameters:
- name: query
type: string
description: |
Search term - can be username, email, display name, or partial matches.
The search is case-insensitive and supports partial matching.
examples: [
"ben@raw-labs.com",
"Benjamin Gaidioz",
"ben",
"benjamin",
"gaidioz"
]
return:
type: array
items:
type: object
properties:
accountId:
type: string
description: The account ID
displayName:
type: string
description: The display name
emailAddress:
type: string
description: The email address
active:
type: boolean
description: Whether the user is active
timeZone:
type: string
description: The user's time zone
tests:
- name: "Search by name"
description: "Verify user search by name returns results"
arguments:
- key: query
value: "Ben"

View File

@@ -0,0 +1,76 @@
# Keycloak Authentication Example
This example demonstrates how to configure MXCP with Keycloak authentication.
## Prerequisites
1. A running Keycloak server (see quick start below)
2. MXCP installed (`pip install mxcp`)
## Quick Start with Docker
Run Keycloak using Docker:
```bash
docker run -p 8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:latest start-dev
```
## Keycloak Setup
1. Access the admin console at http://localhost:8080/admin
2. Login with username: `admin`, password: `admin`
3. Create a new realm (or use the default `master` realm)
4. Create a new client:
- Client ID: `mxcp-demo`
- Client authentication: ON
- Valid redirect URIs: `http://localhost:8000/*`
5. Copy the client secret from the Credentials tab
## Configuration
Set environment variables:
```bash
export KEYCLOAK_CLIENT_ID="mxcp-demo"
export KEYCLOAK_CLIENT_SECRET="your-client-secret"
export KEYCLOAK_REALM="master" # or your custom realm
export KEYCLOAK_SERVER_URL="http://localhost:8080"
```
## Running the Example
1. Start the MXCP server:
```bash
cd examples/keycloak
mxcp serve --debug
```
2. In another terminal, connect with the MCP client:
```bash
mcp connect http://localhost:8000
```
3. You'll be redirected to Keycloak for authentication
## Testing Tools
Once authenticated, try running these example tools:
```bash
# Get current user info
mcp run tool get_user_info
# Query data with user context
mcp run resource user_data
```
## Production Considerations
- Use HTTPS for all URLs in production
- Configure proper redirect URIs
- Set up appropriate Keycloak realm roles and permissions
- Enable refresh token rotation
- Configure session timeouts

View File

@@ -0,0 +1,26 @@
mxcp: 1
projects:
keycloak-demo:
profiles:
dev:
secrets:
- name: keycloak_creds
type: oauth
parameters:
provider: keycloak
auth:
provider: keycloak
keycloak:
client_id: "${KEYCLOAK_CLIENT_ID}"
client_secret: "${KEYCLOAK_CLIENT_SECRET}"
realm: "${KEYCLOAK_REALM}"
server_url: "${KEYCLOAK_SERVER_URL}"
scope: "openid profile email"
callback_path: "/keycloak/callback"
clients:
- client_id: "mcp-cli"
name: "MCP CLI Client"
redirect_uris:
- "http://127.0.0.1:49153/oauth/callback"
scopes:
- "mxcp:access"

View File

@@ -0,0 +1,5 @@
mxcp: 1
project: keycloak-demo
profile: dev
secrets:
- keycloak_creds

View File

@@ -0,0 +1,23 @@
mxcp: 1
tool:
name: get_user_info
description: "Get information about the authenticated user"
parameters: []
return:
type: object
properties:
username:
type: string
description: "Username of the authenticated user"
email:
type: string
description: "Email of the authenticated user"
provider:
type: string
description: "Authentication provider used"
source:
code: |
SELECT
get_username() as username,
get_user_email() as email,
get_user_provider() as provider

View File

@@ -0,0 +1,82 @@
# MXCP Plugin Example
This example demonstrates how to create and use a custom MXCP plugin. The plugin provides various UDFs (User Defined Functions) that can be used in your SQL queries.
## Overview
This plugin implements the Caesar cipher, a simple encryption technique where each letter in the plaintext is shifted by a fixed number of positions in the alphabet.
## Project Structure
```
examples/plugin/
├── plugins/
│ └── my_plugin/
│ └── __init__.py # Plugin implementation
├── tools/
│ └── decipher.yml # Endpoint using the plugin
├── python/ # Directory for Python endpoints
├── sql/ # Directory for SQL implementations
├── config.yml # Example plugin configuration
├── mxcp-site.yml # Project configuration
└── README.md
```
## Configuration
### 1. User Configuration
The example includes two plugin configurations in `config.yml`:
- `rot1`: Rotates letters by 1 position (A->B, B->C, etc.)
- `rot10`: Rotates letters by 10 positions (A->K, B->L, etc.)
To use the plugin, register these configurations in your MXCP user config (`~/.mxcp/config.yml`):
```yaml
mxcp: 1
projects:
demo-plugin:
profiles:
dev:
plugin:
config:
rot1:
rotation: "1"
rot10:
rotation: "10"
```
Then in your `mxcp-site.yml`, you can reference one of these configurations:
```yaml
mxcp: 1
project: demo-plugin
profile: dev
plugin:
- name: str_secret
module: my_plugin
config: rot1
- name: tricky
module: my_plugin
config: rot10
```
## Running the MCP
To run the service using the example configuration:
1. Set the `MXCP_CONFIG` environment variable to point to the example's config file:
```bash
export MXCP_CONFIG=/path/to/examples/plugin/config.yml
```
2. Start the MXCP server:
```bash
mxcp serve
```
The service will now use the example configuration with both the `simple` (rot1) and `tricky` (rot10) Caesar cipher plugins.

View File

@@ -0,0 +1,12 @@
mxcp: 1
projects:
demo-plugin:
profiles:
dev:
plugin:
config:
rot1:
rotation: "1" # Rotate by 1 position (A->B, B->C, etc.)
rot10:
rotation: "10" # Rotate by 10 positions (A->K, B->L, etc.)

View File

@@ -0,0 +1,7 @@
mxcp: 1
project: demo-plugin
profile: dev
plugin:
- name: str_secret
module: my_plugin
config: rot1

View File

@@ -0,0 +1,33 @@
# MXCP Plugins Directory
This directory contains MXCP plugins that extend DuckDB with custom User Defined Functions (UDFs).
## Structure
Each plugin should be a Python module containing a class named `MXCPPlugin` that inherits from `MXCPBasePlugin`.
```
plugins/
├── my_plugin/
│ └── __init__.py # Contains MXCPPlugin class
├── utils/
│ └── string_utils.py
└── integrations/
└── api_plugin.py
```
## Usage
Plugins are referenced in `mxcp-site.yml`:
```yaml
plugin:
- name: cipher
module: my_plugin
config: rot13
```
The functions are then available in SQL as `{function_name}_{plugin_name}`:
```sql
SELECT encrypt_cipher('hello world');
```

View File

@@ -0,0 +1,134 @@
"""
Example MXCP Plugin
This plugin demonstrates how to create a simple MXCP plugin with Caesar cipher encryption capabilities
and optional user context integration for authentication-aware features.
Example usage:
>>> plugin = MXCPPlugin({"rotation": 13})
>>> plugin.encrypt("Hello, World!") # Returns "Uryyb, Jbeyq!"
>>> plugin.decrypt("Uryyb, Jbeyq!") # Returns "Hello, World!"
With user context (when authentication is enabled):
>>> plugin.get_user_info() # Returns user information
"""
from typing import Any, Dict, Optional
from mxcp.plugins import MXCPBasePlugin, udf
class MXCPPlugin(MXCPBasePlugin):
"""Plugin that provides Caesar cipher encryption and decryption functions.
This plugin implements the Caesar cipher, a type of substitution cipher where
each letter in the plaintext is shifted a certain number of places down or up
the alphabet. It also demonstrates how to use user context for authentication-aware features.
Example:
>>> plugin = MXCPPlugin({"rotation": 13})
>>> plugin.encrypt("Hello, World!") # Returns "Uryyb, Jbeyq!"
>>> plugin.decrypt("Uryyb, Jbeyq!") # Returns "Hello, World!"
"""
def __init__(self, config: Dict[str, Any], user_context=None):
"""Initialize the plugin with configuration and optional user context.
Args:
config: Configuration dictionary containing:
- rotation: Number of positions to shift (1-25), can be string or int
user_context: Optional authenticated user context (for new plugins)
"""
super().__init__(config, user_context)
rotation = config.get("rotation", 13)
# Convert string to int if needed
if isinstance(rotation, str):
try:
rotation = int(rotation)
except ValueError:
raise ValueError("Rotation must be a valid integer")
if not isinstance(rotation, int) or rotation < 1 or rotation > 25:
raise ValueError("Rotation must be an integer between 1 and 25")
self.rotation = rotation
def __rotate_char(self, char: str, forward: bool = True) -> str:
"""Rotate a single character by the configured number of positions.
Args:
char: Character to rotate
forward: True for encryption, False for decryption
Returns:
Rotated character
"""
if not char.isalpha():
return char
# Determine the base ASCII value (a=97, A=65)
base = ord("a") if char.islower() else ord("A")
# Calculate the position in the alphabet (0-25)
pos = ord(char) - base
# Apply rotation (forward or backward)
shift = self.rotation if forward else -self.rotation
# Wrap around the alphabet and convert back to character
return chr(base + ((pos + shift) % 26))
@udf
def encrypt(self, text: str) -> str:
"""Encrypt text using the Caesar cipher.
Args:
text: Text to encrypt
Returns:
Encrypted text
Example:
>>> plugin.encrypt("Hello, World!") # Returns "Uryyb, Jbeyq!"
"""
return "".join(self.__rotate_char(c, True) for c in text)
@udf
def decrypt(self, text: str) -> str:
"""Decrypt text using the Caesar cipher.
Args:
text: Text to decrypt
Returns:
Decrypted text
Example:
>>> plugin.decrypt("Uryyb, Jbeyq!") # Returns "Hello, World!"
"""
return "".join(self.__rotate_char(c, False) for c in text)
@udf
def encrypt_with_user_key(self, text: str) -> str:
"""Encrypt text using a user-specific rotation based on their username.
This demonstrates how plugins can use user context to provide
personalized functionality.
Args:
text: Text to encrypt
Returns:
Text encrypted with user-specific key, or standard encryption if not authenticated
"""
if self.is_authenticated():
# Use username length as additional rotation factor
username = self.get_username() or ""
user_rotation = (self.rotation + len(username)) % 26
# Temporarily modify rotation for this operation
original_rotation = self.rotation
self.rotation = user_rotation if user_rotation > 0 else 1
result = self.encrypt(text)
self.rotation = original_rotation # Restore original rotation
return result
else:
# Fall back to standard encryption
return self.encrypt(text)

View File

@@ -0,0 +1,20 @@
mxcp: 1
tool:
name: "decipher"
description: "Decrypt an encrypted message."
parameters:
- name: message
type: string
description: "Encrypted message"
return:
type: string
source:
code: SELECT decrypt_str_secret($message);
annotations:
readOnlyHint: true
tests:
- name: quick_check
arguments:
- key: message
value: "usbmbmb"

View File

@@ -0,0 +1,58 @@
# Python Endpoints Demo
This example demonstrates how to create and use Python-based endpoints in MXCP.
## Features Demonstrated
### 1. Basic Python Functions
- `analyze_numbers` - Statistical analysis with various operations
- `create_sample_data` - Database operations from Python
### 2. Async Functions
- `process_time_series` - Demonstrates async Python endpoint
### 3. Database Access
- Using `mxcp.runtime.db` to execute SQL queries
- Parameter binding for safe SQL execution
## Running the Examples
In a terminal, test the endpoints:
```bash
# Create sample data
mxcp run tool create_sample_data --param table_name=test_data --param row_count=100
# Analyze numbers
mxcp run tool analyze_numbers --param numbers="[1, 2, 3, 4, 5]" --param operation=mean
# Process time series (async function)
mxcp run tool process_time_series --param table_name=test_data --param window_days=7
```
Or, if you prefer, you can also start the MXCP server and use any MCP client to call the tools:
```bash
mxcp serve
```
## Project Structure
```
python-demo/
├── mxcp-site.yml # Project configuration
├── python/ # Python modules
│ └── data_analysis.py # Python endpoint implementations
├── tools/ # Tool definitions
│ ├── analyze_numbers.yml
│ ├── create_sample_data.yml
│ └── process_time_series.yml
└── README.md
```
## Key Concepts
1. **Language Declaration**: Set `language: python` in the tool definition
2. **Function Names**: The function name must match the tool name
3. **Return Types**: Functions must return data matching the declared return type
4. **Database Access**: Use `db.execute()` for SQL queries
5. **Async Support**: Both sync and async functions are supported

View File

@@ -0,0 +1,3 @@
mxcp: 1
project: python-demo
profile: default

View File

@@ -0,0 +1,145 @@
"""
Example Python endpoints for data analysis.
"""
import statistics
from datetime import datetime, timedelta
from mxcp.runtime import config, db
def analyze_numbers(numbers: list, operation: str = "mean") -> dict:
"""
Analyze a list of numbers with various statistical operations.
"""
if not numbers:
return {"error": "No numbers provided"}
operations = {
"mean": statistics.mean,
"median": statistics.median,
"mode": statistics.mode,
"stdev": statistics.stdev if len(numbers) > 1 else lambda x: 0,
"sum": sum,
"min": min,
"max": max,
}
if operation not in operations:
return {"error": f"Unknown operation: {operation}"}
try:
result = operations[operation](numbers)
return {"operation": operation, "result": result, "count": len(numbers)}
except Exception as e:
return {"error": str(e)}
def create_sample_data(table_name: str, row_count: int) -> dict:
"""
Create a sample table with test data.
"""
try:
# Drop table if exists
db.execute(f"DROP TABLE IF EXISTS {table_name}")
# Create table
db.execute(
f"""
CREATE TABLE {table_name} (
id INTEGER PRIMARY KEY,
name VARCHAR,
value DOUBLE,
category VARCHAR,
created_at TIMESTAMP
)
"""
)
# Insert sample data
for i in range(row_count):
db.execute(
f"""
INSERT INTO {table_name} (id, name, value, category, created_at)
VALUES (
$id,
'Item ' || $item_num,
ROUND(RANDOM() * 1000, 2),
CASE
WHEN RANDOM() < 0.33 THEN 'A'
WHEN RANDOM() < 0.66 THEN 'B'
ELSE 'C'
END,
CURRENT_TIMESTAMP - INTERVAL ($days || ' days')
)
""",
{"id": i + 1, "item_num": i + 1, "days": i % 30},
)
return {"status": "success", "table": table_name, "rows_created": row_count}
except Exception as e:
return {"status": "error", "error": str(e)}
def aggregate_by_category(table_name: str) -> list:
"""
Aggregate data by category from a table.
"""
try:
results = db.execute(
f"""
SELECT
category,
COUNT(*) as count,
ROUND(AVG(value), 2) as avg_value,
ROUND(SUM(value), 2) as total_value,
MIN(value) as min_value,
MAX(value) as max_value
FROM {table_name}
GROUP BY category
ORDER BY category
"""
)
return results
except Exception as e:
return [{"error": str(e)}]
async def process_time_series(table_name: str, window_days: int = 7) -> list:
"""
Async function to process time series data with rolling windows.
"""
import asyncio
# Simulate some async processing
await asyncio.sleep(0.1)
results = db.execute(
f"""
WITH daily_data AS (
SELECT
DATE_TRUNC('day', created_at) as date,
category,
COUNT(*) as daily_count,
ROUND(AVG(value), 2) as daily_avg
FROM {table_name}
GROUP BY DATE_TRUNC('day', created_at), category
)
SELECT
date,
category,
daily_count,
daily_avg,
ROUND(AVG(daily_avg) OVER (
PARTITION BY category
ORDER BY date
ROWS BETWEEN {window_days - 1} PRECEDING AND CURRENT ROW
), 2) as rolling_avg
FROM daily_data
ORDER BY date DESC, category
LIMIT 50
"""
)
return results

View File

@@ -0,0 +1,16 @@
"""Example showing Python endpoints can return primitive arrays."""
def show_primitive_arrays() -> list:
"""Return Fibonacci sequence as array of numbers."""
return [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
def get_languages() -> list:
"""Return list of programming languages."""
return ["Python", "JavaScript", "Go", "Rust", "TypeScript"]
def get_pi_digits() -> list:
"""Return digits of pi."""
return [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]

View File

@@ -0,0 +1,28 @@
mxcp: 1
tool:
name: aggregate_by_category
description: Aggregate data by category with statistics
language: python
source:
file: ../python/data_analysis.py
parameters:
- name: table_name
type: string
description: Name of the table to aggregate
return:
type: array
items:
type: object
properties:
category:
type: string
count:
type: integer
avg_value:
type: number
total_value:
type: number
min_value:
type: number
max_value:
type: number

View File

@@ -0,0 +1,27 @@
mxcp: 1
tool:
name: analyze_numbers
description: Analyze a list of numbers with statistical operations
language: python
source:
file: ../python/data_analysis.py
parameters:
- name: numbers
type: array
items:
type: number
description: List of numbers to analyze
- name: operation
type: string
enum: ["mean", "median", "mode", "stdev", "sum", "min", "max"]
default: "mean"
description: Statistical operation to perform
return:
type: object
properties:
operation:
type: string
result:
type: number
count:
type: integer

View File

@@ -0,0 +1,25 @@
mxcp: 1
tool:
name: create_sample_data
description: Create a sample table with test data
language: python
source:
file: ../python/data_analysis.py
parameters:
- name: table_name
type: string
description: Name of the table to create
- name: row_count
type: integer
description: Number of rows to generate
minimum: 1
maximum: 10000
return:
type: object
properties:
status:
type: string
table:
type: string
rows_created:
type: integer

View File

@@ -0,0 +1,21 @@
mxcp: 1
tool:
name: process_time_series
description: Process time series data with rolling window calculations (async)
language: python
source:
file: ../python/data_analysis.py
parameters:
- name: table_name
type: string
description: Name of the table containing time series data
- name: window_days
type: integer
default: 7
description: Size of the rolling window in days
minimum: 1
maximum: 365
return:
type: array
items:
type: object

View File

@@ -0,0 +1,185 @@
# Salesforce OAuth Demo
This example demonstrates how to create MCP tools that interact with Salesforce using the MXCP OAuth authentication system with the `simple_salesforce` library.
## Features Demonstrated
### 1. MXCP OAuth Authentication
- Project-wide Salesforce OAuth configuration
- Automatic token management through MXCP authentication system
- User authentication via standard OAuth 2.0 flow
- Error handling for authentication failures
### 2. Salesforce API Integration
- `list_sobjects` - Retrieve all available Salesforce objects (sObjects) from your org with optional filtering
- `describe_sobject` - Get detailed metadata for a specific Salesforce object, including field information
- `get_sobject` - Retrieve a specific Salesforce record by its ID
- `search` - Search across all searchable Salesforce objects using simple search terms
- `soql` - Execute SOQL (Salesforce Object Query Language) queries
- `sosl` - Execute SOSL (Salesforce Object Search Language) queries for complex searches
- `whoami` - Display information about the current authenticated Salesforce user
- Token-based API access using authenticated user context
## Prerequisites
1. **Salesforce Org**: You need access to a Salesforce org (Developer Edition is fine)
2. **Salesforce Connected App**: Create a Connected App in Salesforce with OAuth settings
3. **Python Dependencies**: The `simple_salesforce` library (automatically managed by MXCP)
## Setup
### 1. Create Salesforce Connected App
1. Log into your Salesforce org
2. Go to **Setup****App Manager****New Connected App**
3. Fill in basic information:
- **Connected App Name**: "MXCP Integration" (or your preferred name)
- **API Name**: Will auto-populate
- **Contact Email**: Your email
4. Enable OAuth Settings:
- **Enable OAuth Settings**: Check this box
- **Callback URL**: This depends on your deployment:
- **Local Development**: `http://localhost:8000/salesforce/callback`
- **Remote/Production**: `https://your-domain.com/salesforce/callback` (replace with your actual server URL)
- **Selected OAuth Scopes**: Add these scopes:
- Access and manage your data (api)
- Perform requests on your behalf at any time (refresh_token, offline_access)
- Access your basic information (id, profile, email, address, phone)
5. Save the Connected App
6. Note down the **Consumer Key** (Client ID) and **Consumer Secret** (Client Secret)
### 2. Configure Environment Variables
Set your Salesforce OAuth credentials:
```bash
export SALESFORCE_CLIENT_ID="your-consumer-key-from-connected-app"
export SALESFORCE_CLIENT_SECRET="your-consumer-secret-from-connected-app"
```
### 3. Configure Callback URL for Your Deployment
The callback URL configuration depends on where your MXCP server will run:
#### Local Development
For local development, the default configuration in `config.yml` uses `http://localhost:8000/salesforce/callback`. This works when:
- You're running MXCP locally on your development machine
- Users authenticate from the same machine where MXCP is running
#### Remote/Production Deployment
For remote servers or production deployments, you need to:
1. **Update config.yml**: Uncomment and modify the production callback URL:
```yaml
redirect_uris:
- "http://localhost:8000/salesforce/callback" # Keep for local dev
- "https://your-domain.com/salesforce/callback" # Add your actual URL
```
2. **Update base_url**: Set the correct base URL in your config:
```yaml
transport:
http:
base_url: https://your-domain.com # Your actual server URL
```
3. **Configure Connected App**: Add the production callback URL to your Salesforce Connected App's callback URLs
**Important**:
- The callback URL must be accessible from the user's browser, not just from your server
- For production deployments, Salesforce requires HTTPS for callback URLs
- You can configure multiple callback URLs in your Connected App to support both local development and production
## Authenticate with Salesforce
When you first run MXCP, you'll need to authenticate with Salesforce:
```bash
# Start the MXCP server with the config file - this will prompt for authentication
MXCP_CONFIG=config.yml mxcp serve
```
The authentication flow will:
1. Open your browser to Salesforce login
2. You'll log in with your Salesforce credentials
3. Authorize the MXCP application
4. Redirect back to complete authentication
## Project Structure
```
salesforce-oauth/
├── mxcp-site.yml # Project metadata
├── config.yml # Server and authentication configuration
├── python/ # Python modules
│ └── salesforce_client.py # Salesforce API implementations
├── tools/ # Tool definitions
│ ├── list_sobjects.yml # List all Salesforce objects
│ ├── describe_sobject.yml # Get object metadata
│ ├── get_sobject.yml # Get record by ID
│ ├── search.yml # Search across objects
│ ├── soql.yml # Execute SOQL queries
│ ├── sosl.yml # Execute SOSL queries
│ └── whoami.yml # Current user information
└── README.md # This file
```
## Key Concepts
1. **MXCP OAuth Integration**: Uses MXCP's built-in Salesforce OAuth provider for secure authentication
2. **User Context**: Access tokens are automatically managed and provided through `user_context()`
3. **Token-based Authentication**: simple_salesforce is initialized with OAuth tokens instead of credentials
4. **Project-wide Configuration**: Authentication is configured at the project level in `mxcp-site.yml`
5. **Error Handling**: Comprehensive error handling for authentication and API failures
6. **API Integration**: Demonstrates calling Salesforce REST API endpoints with proper OAuth tokens
## Example Output
When you run `list_sobjects`, you'll get a response like:
```json
[
"Account",
"Contact",
"Lead",
"Opportunity",
"Case",
"Product2",
"Task",
"Event",
"User",
"CustomObject__c",
...
]
```
## Troubleshooting
### Authentication Errors
- **"No user context available"**: User needs to authenticate first by running `mxcp serve` and completing OAuth flow
- **"No Salesforce access token found"**: Authentication was incomplete or token expired - re-authenticate
- **Connected App Issues**: Verify your `SALESFORCE_CLIENT_ID` and `SALESFORCE_CLIENT_SECRET` are correct
- **Callback URL Mismatch**: Ensure the callback URL in your Connected App matches where your MXCP server is accessible:
- Local development: `http://localhost:8000/salesforce/callback`
- Remote/production: `https://your-domain.com/salesforce/callback`
- **OAuth Scopes**: Verify your Connected App has the required OAuth scopes (api, refresh_token, id, profile, email)
### API Errors
- Verify you have the necessary permissions in Salesforce
- Check that your org is accessible and not in maintenance mode
- Ensure your Connected App is approved and not restricted by IP ranges
### Connected App Setup Issues
- **App Not Found**: Make sure your Connected App is saved and the Consumer Key/Secret are copied correctly
- **Callback URL**: The callback URL must exactly match your MXCP server's accessible address:
- For local development: `http://localhost:8000/salesforce/callback`
- For remote deployment: `https://your-domain.com/salesforce/callback`
- **OAuth Scopes**: Missing scopes will cause authentication to fail - ensure all required scopes are selected
## Next Steps
This example demonstrates a comprehensive set of Salesforce integration tools. You could extend it with additional tools for data manipulation like:
- `create_record` - Create new records in Salesforce objects
- `update_record` - Update existing records
- `delete_record` - Delete records
- `bulk_operations` - Handle bulk data operations for large datasets

View File

@@ -0,0 +1,22 @@
mxcp: 1
transport:
http:
port: 8000
host: 0.0.0.0
# Set base_url to your server's public URL for production
base_url: http://localhost:8000
projects:
salesforce-oauth:
profiles:
default:
# OAuth Authentication Configuration
auth:
provider: salesforce
salesforce:
client_id: "${SALESFORCE_CLIENT_ID}"
client_secret: "${SALESFORCE_CLIENT_SECRET}"
scope: "api refresh_token openid profile email"
callback_path: "/salesforce/callback"
auth_url: "https://login.salesforce.com/services/oauth2/authorize"
token_url: "https://login.salesforce.com/services/oauth2/token"

View File

@@ -0,0 +1,3 @@
mxcp: 1
project: salesforce-oauth
profile: default

View File

@@ -0,0 +1,466 @@
"""
Salesforce MCP tools using simple_salesforce with MXCP OAuth authentication.
"""
import threading
from functools import wraps
from typing import Dict, Any, List, Optional
from mxcp.sdk.auth.context import get_user_context
from simple_salesforce import Salesforce # type: ignore[attr-defined]
from simple_salesforce.exceptions import SalesforceExpiredSession
from mxcp.runtime import on_init, on_shutdown
import logging
# Thread-safe cache for Salesforce clients
_client_cache: Optional[Dict[str, Salesforce]] = None
_cache_lock: Optional[threading.Lock] = None
@on_init
def init_client_cache() -> None:
"""
Initialize the Salesforce client cache.
"""
global _client_cache, _cache_lock
_client_cache = {}
_cache_lock = threading.Lock()
@on_shutdown
def clear_client_cache() -> None:
"""
Clear the Salesforce client cache.
"""
global _client_cache, _cache_lock
_client_cache = None
_cache_lock = None
def _get_cache_key(context: Any) -> Optional[str]:
"""Generate a cache key based on user context."""
if not context:
return None
# Use user ID and instance URL as cache key
user_id = getattr(context, "user_id", None) or getattr(context, "id", None)
# Extract instance URL
instance_url = None
if context.raw_profile and "urls" in context.raw_profile:
urls = context.raw_profile["urls"]
instance_url = urls.get("custom_domain")
if not instance_url:
for url_key in ["rest", "enterprise", "partner"]:
if url_key in urls:
service_url = urls[url_key]
instance_url = service_url.split("/services/")[0]
break
if user_id and instance_url:
return f"{user_id}:{instance_url}"
return None
def with_session_retry(func: Any) -> Any:
"""
Decorator that automatically retries API calls with cache invalidation when sessions expire.
This handles the race condition where a session might expire between cache validation
and the actual API call.
"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
return func(*args, **kwargs)
except SalesforceExpiredSession:
logging.error("Salesforce session expired")
# Session expired during the call - invalidate cache and retry once
context = get_user_context()
cache_key = _get_cache_key(context)
if cache_key and _cache_lock and _client_cache:
with _cache_lock:
# Remove the expired client from cache
_client_cache.pop(cache_key, None)
# Retry the function call - this will get a fresh client
return func(*args, **kwargs)
return wrapper
def _escape_sosl_search_term(search_term: str) -> str:
"""
Escape special characters in SOSL search terms to prevent injection attacks.
SOSL special characters that need escaping: & | ! { } [ ] ( ) ^ ~ * ? : " ' + -
"""
# Escape backslashes first to avoid double-escaping
escaped = search_term.replace("\\", "\\\\")
# Escape SOSL special characters
special_chars = [
"&",
"|",
"!",
"{",
"}",
"[",
"]",
"(",
")",
"^",
"~",
"*",
"?",
":",
'"',
"'",
"+",
"-",
]
for char in special_chars:
escaped = escaped.replace(char, f"\\{char}")
return escaped
def _get_salesforce_client() -> Salesforce:
"""
Create and return an authenticated Salesforce client using OAuth tokens from user_context.
Uses caching to avoid recreating clients unnecessarily. Clients are cached per user
and instance URL combination in a thread-safe manner.
"""
try:
# Get the authenticated user's context
context = get_user_context()
if not context:
raise ValueError("No user context available. User must be authenticated.")
# Generate cache key
cache_key = _get_cache_key(context)
# Try to get cached client first
if cache_key and _cache_lock and _client_cache:
with _cache_lock:
if cache_key in _client_cache:
logging.info("Using cached Salesforce client")
# Return cached client - retry decorator will handle any session expiry
return _client_cache[cache_key]
logging.info("No cached Salesforce client found, creating new one")
# Extract Salesforce OAuth tokens from user context
access_token = context.external_token
# Extract instance URL from user context (this is user/org-specific)
instance_url = None
if context.raw_profile and "urls" in context.raw_profile:
urls = context.raw_profile["urls"]
# Try custom_domain first (this is the full instance URL)
instance_url = urls.get("custom_domain")
if not instance_url:
# Fallback: extract base URL from any service endpoint
for url_key in ["rest", "enterprise", "partner"]:
if url_key in urls:
service_url = urls[url_key]
instance_url = service_url.split("/services/")[0]
break
if not access_token:
raise ValueError(
"No Salesforce access token found in user context. "
"User must authenticate with Salesforce through MXCP."
)
if not instance_url:
raise ValueError(
"No Salesforce instance URL found in user context. "
"Authentication may be incomplete or profile missing URL information."
)
# Initialize Salesforce client with OAuth token
sf = Salesforce(session_id=access_token, instance_url=instance_url)
# Cache the client if we have a valid cache key
if cache_key and _cache_lock and _client_cache:
with _cache_lock:
_client_cache[cache_key] = sf
return sf
except Exception as e:
raise ValueError(f"Failed to authenticate with Salesforce: {str(e)}")
@with_session_retry
def list_sobjects(filter: Optional[str] = None) -> List[str]:
"""
List all available Salesforce objects (sObjects) in the org.
Args:
filter: Optional fuzzy filter to match object names (case-insensitive substring search).
Examples: "account", "__c" for custom objects, "contact", etc.
Returns:
list: List of Salesforce object names as strings
"""
try:
sf = _get_salesforce_client()
# Get all sObjects metadata
describe_result = sf.describe()
if not describe_result or "sobjects" not in describe_result:
raise ValueError("Invalid describe response from Salesforce API")
# Extract just the object names
sobjects = describe_result["sobjects"]
object_names = []
for obj in sobjects:
if not isinstance(obj, dict) or "name" not in obj:
raise ValueError(f"Invalid sobject format: {obj}")
object_names.append(obj["name"])
# Apply fuzzy filter if provided
if filter is not None and filter.strip():
filter_lower = filter.lower()
object_names = [name for name in object_names if filter_lower in name.lower()]
# Sort alphabetically for consistent output
object_names.sort()
return object_names
except Exception as e:
# Return error in a format that can be handled by the caller
raise Exception(f"Error listing Salesforce objects: {str(e)}")
@with_session_retry
def describe_sobject(object_name: str) -> Dict[str, Any]:
"""
Get detailed field information for a specific Salesforce object (sObject).
Args:
object_name: The API name of the Salesforce object to describe
Returns:
dict: Dictionary where each key is a field name and each value contains field metadata
"""
sf = _get_salesforce_client()
# Try to get the object - catch this specifically for "object doesn't exist"
try:
sobject = getattr(sf, object_name)
except AttributeError:
raise Exception(f"Salesforce object '{object_name}' does not exist")
# Let API errors from describe() propagate naturally with their original messages
describe_result = sobject.describe()
if not describe_result or "fields" not in describe_result:
raise ValueError(f"Invalid describe response for object '{object_name}'")
# Process fields into the required format
fields_info = {}
for field in describe_result["fields"]:
if not isinstance(field, dict):
raise ValueError(f"Invalid field format in '{object_name}': {field}")
required_fields = ["name", "type", "label"]
for required_field in required_fields:
if required_field not in field:
raise ValueError(f"Field missing '{required_field}' in '{object_name}': {field}")
field_name = field["name"]
field_info = {"type": field["type"], "label": field["label"]}
# Add referenceTo information for reference fields
if field["type"] == "reference" and field.get("referenceTo"):
field_info["referenceTo"] = field["referenceTo"]
fields_info[field_name] = field_info
return fields_info
@with_session_retry
def get_sobject(object_name: str, record_id: str) -> Dict[str, Any]:
"""
Retrieve a specific Salesforce record by its object type and ID.
Args:
object_name: The API name of the Salesforce object type
record_id: The unique Salesforce ID of the record to retrieve
Returns:
dict: Dictionary containing all fields and values for the specified record
"""
sf = _get_salesforce_client()
# Try to get the object - catch this specifically for "object doesn't exist"
try:
sobject = getattr(sf, object_name)
except AttributeError:
raise Exception(f"Salesforce object '{object_name}' does not exist")
# Let API errors from get() propagate naturally with their original messages
record = sobject.get(record_id)
if not isinstance(record, dict):
raise ValueError(f"Invalid record format returned for {object_name}:{record_id}")
# Remove 'attributes' field for consistency with other functions
clean_record: Dict[str, Any] = {k: v for k, v in record.items() if k != "attributes"}
return clean_record
@with_session_retry
def soql(query: str) -> List[Dict[str, Any]]:
"""
Execute an arbitrary SOQL (Salesforce Object Query Language) query.
Args:
query: The SOQL query to execute
Returns:
list: Array of records returned by the SOQL query
"""
sf = _get_salesforce_client()
# Execute the SOQL query
result = sf.query(query)
if not result or "records" not in result:
raise ValueError("Invalid SOQL query response from Salesforce API")
# Remove 'attributes' field from each record for cleaner output
records = []
for record in result["records"]:
if not isinstance(record, dict):
raise ValueError(f"Invalid record format in SOQL result: {record}")
clean_record = {k: v for k, v in record.items() if k != "attributes"}
records.append(clean_record)
return records
@with_session_retry
def search(search_term: str) -> List[Dict[str, Any]]:
"""
Search for records across all searchable Salesforce objects using a simple search term.
Uses Salesforce's native search to automatically find matches across all objects.
Args:
search_term: The term to search for across Salesforce objects
Returns:
list: Array of matching records from various Salesforce objects
"""
sf = _get_salesforce_client()
# Escape the search term to prevent SOSL injection attacks
escaped_search_term = _escape_sosl_search_term(search_term)
# Use simple SOSL syntax - Salesforce searches all searchable objects automatically
sosl_query = f"FIND {{{escaped_search_term}}}"
# Execute the SOSL search
search_results = sf.search(sosl_query)
if not search_results or "searchRecords" not in search_results:
raise ValueError("Invalid SOSL search response from Salesforce API")
# Flatten results from all objects into a single array
all_records = []
for record in search_results["searchRecords"]:
if not isinstance(record, dict):
raise ValueError(f"Invalid record format in SOSL result: {record}")
if "attributes" not in record or not isinstance(record["attributes"], dict):
raise ValueError(f"Invalid record attributes in SOSL result: {record}")
# Remove 'attributes' field and add object type for context
clean_record = {k: v for k, v in record.items() if k != "attributes"}
clean_record["_ObjectType"] = record["attributes"]["type"]
all_records.append(clean_record)
return all_records
@with_session_retry
def sosl(query: str) -> List[Dict[str, Any]]:
"""
Execute an arbitrary SOSL (Salesforce Object Search Language) query.
Args:
query: The SOSL query to execute
Returns:
list: Array of records returned by the SOSL search query
"""
sf = _get_salesforce_client()
# Execute the SOSL search
search_results = sf.search(query)
if not search_results or "searchRecords" not in search_results:
raise ValueError("Invalid SOSL query response from Salesforce API")
# Flatten results from all objects into a single array
all_records = []
for record in search_results["searchRecords"]:
if not isinstance(record, dict):
raise ValueError(f"Invalid record format in SOSL result: {record}")
if "attributes" not in record or not isinstance(record["attributes"], dict):
raise ValueError(f"Invalid record attributes in SOSL result: {record}")
# Remove 'attributes' field and add object type for context
clean_record = {k: v for k, v in record.items() if k != "attributes"}
clean_record["_ObjectType"] = record["attributes"]["type"]
all_records.append(clean_record)
return all_records
def whoami() -> Dict[str, Any]:
"""
Get basic information about the currently authenticated Salesforce user from the user context.
Returns essential user information from the MXCP authentication context without making API calls.
Returns:
dict: Dictionary containing essential current user information
"""
context = get_user_context()
if not context:
raise ValueError("No user context available. User must be authenticated.")
# Extract instance URL from context
instance_url = None
if context.raw_profile and "urls" in context.raw_profile:
urls = context.raw_profile["urls"]
instance_url = urls.get("custom_domain")
if not instance_url:
# Fallback: extract base URL from any service endpoint
for url_key in ["rest", "enterprise", "partner"]:
if url_key in urls:
service_url = urls[url_key]
instance_url = service_url.split("/services/")[0]
break
# Extract essential user information from raw profile
raw_profile = context.raw_profile or {}
user_info = {
"user_id": raw_profile.get("user_id"),
"email": raw_profile.get("email"),
"name": raw_profile.get("name"),
"preferred_username": raw_profile.get("preferred_username"),
"organization_id": raw_profile.get("organization_id"),
"instanceUrl": instance_url,
}
return user_info

View File

@@ -0,0 +1,34 @@
mxcp: 1
tool:
name: describe_sobject
description: |
Get detailed metadata for a specific Salesforce object, including all field information.
Returns field names, types, labels, and relationship details.
tags: ["salesforce", "metadata", "schema"]
annotations:
title: "Describe Salesforce Object"
readOnlyHint: true
idempotentHint: true
parameters:
- name: object_name
type: string
description: "Name of the Salesforce object to describe"
examples: ["Account", "Contact", "Opportunity", "Lead", "Case"]
return:
type: object
description: "Object metadata with field information"
additionalProperties: true
language: python
source:
file: ../python/salesforce_client.py
tests:
- name: "Describe Account object"
description: "Test describing the standard Account object"
arguments:
- key: object_name
value: "Account"
result_contains:
Name:
type: "string"
Id:
type: "id"

View File

@@ -0,0 +1,37 @@
mxcp: 1
tool:
name: get_sobject
description: |
Retrieve a specific Salesforce record by its ID.
Returns the complete record data with all accessible fields.
tags: ["salesforce", "data", "records"]
annotations:
title: "Get Salesforce Record"
readOnlyHint: true
idempotentHint: true
parameters:
- name: object_name
type: string
description: "Name of the Salesforce object type"
examples: ["Account", "Contact", "Opportunity", "Lead", "Case"]
- name: record_id
type: string
description: "Salesforce record ID (15 or 18 character ID)"
examples: ["001000000000001", "003000000000001AAA"]
return:
type: object
description: "Complete record data"
additionalProperties: true
language: python
source:
file: ../python/salesforce_client.py
tests:
- name: "Get Account record"
description: "Test retrieving an Account record by ID"
arguments:
- key: object_name
value: "Account"
- key: record_id
value: "001000000000001"
result_contains:
Id: "001000000000001"

View File

@@ -0,0 +1,38 @@
mxcp: 1
tool:
name: list_sobjects
description: |
List all available Salesforce objects (sObjects) in the organization.
Optionally filter the list by providing a filter term for fuzzy matching on object names.
tags: ["salesforce", "metadata", "objects"]
annotations:
title: "List Salesforce Objects"
readOnlyHint: true
idempotentHint: true
parameters:
- name: filter
type: string
description: "Optional filter term to match against object names (case-insensitive fuzzy matching)"
default: null
examples: ["Account", "Contact", "Custom"]
return:
type: array
description: "List of sObject names"
items:
type: string
description: "Name of a Salesforce object"
language: python
source:
file: ../python/salesforce_client.py
tests:
- name: "List all objects"
description: "Test listing all available Salesforce objects"
arguments: []
result_contains_item: "Account"
- name: "Filter objects"
description: "Test filtering objects by name"
arguments:
- key: filter
value: "Account"
result_contains_item: "Account"

View File

@@ -0,0 +1,38 @@
mxcp: 1
tool:
name: search
description: |
Search across all searchable Salesforce objects using the native Salesforce search.
This uses the simple SOSL syntax "FIND {search_term}" which automatically searches
all searchable objects and fields.
language: python
source:
file: ../python/salesforce_client.py
parameters:
- name: search_term
type: string
description: "Term to search for across all searchable objects"
examples: ["John", "Acme", "555-1234", "example.com"]
return:
type: array
description: "Search results from all matching objects"
items:
type: object
description: "Search result record"
additionalProperties: true
tags:
- salesforce
- search
- data
annotations:
title: "Search Salesforce Records"
readOnlyHint: true
idempotentHint: true
tests:
- name: "Basic search"
description: "Test searching for a common term"
arguments:
- key: search_term
value: "test"
# Note: Using result type array since search results can be empty or contain records
result: []

View File

@@ -0,0 +1,33 @@
mxcp: 1
tool:
name: soql
description: |
Execute a SOQL (Salesforce Object Query Language) query. Returns query results as an array of records.
For personalized queries (e.g., 'my tasks', 'my opportunities'), use the whoami tool first to get the current user's ID for filtering (e.g., WHERE OwnerId = 'user_id').
tags: ["salesforce", "query", "data"]
annotations:
title: "Execute SOQL Query"
readOnlyHint: true
idempotentHint: true
parameters:
- name: query
type: string
description: "SOQL query string to execute"
examples: ["SELECT Id, Name FROM Account LIMIT 10", "SELECT Id, Email FROM Contact WHERE LastName = 'Smith'"]
return:
type: array
description: "Query results"
items:
type: object
description: "Record data"
additionalProperties: true
language: python
source:
file: ../python/salesforce_client.py
tests:
- name: "Simple Account query"
description: "Test executing a basic SOQL query on Account object"
arguments:
- key: query
value: "SELECT Id, Name FROM Account LIMIT 1"
result_length: 1

View File

@@ -0,0 +1,36 @@
mxcp: 1
tool:
name: sosl
description: |
Execute a raw SOSL (Salesforce Object Search Language) query.
Allows complex search queries with specific object targeting and field selection.
language: python
source:
file: ../python/salesforce_client.py
parameters:
- name: query
type: string
description: "SOSL query string to execute"
examples: ["FIND {test} IN ALL FIELDS RETURNING Account(Id, Name)", "FIND {John} RETURNING Contact(Id, Name, Email)"]
return:
type: array
description: "Search results"
items:
type: object
description: "Search result record"
additionalProperties: true
tags:
- salesforce
- search
- advanced
annotations:
title: "Execute SOSL Query"
readOnlyHint: true
idempotentHint: true
tests:
- name: "Simple SOSL query"
description: "Test executing a basic SOSL search query"
arguments:
- key: query
value: "FIND {test} IN ALL FIELDS RETURNING Account(Id, Name)"
result: []

Some files were not shown because too many files have changed in this diff Show More