#
tokens: 20031/50000 9/9 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .dockerignore
├── .github
│   └── workflows
│       ├── ci.yaml
│       └── docker-publish.yml
├── .gitignore
├── .goreleaser.yml
├── Dockerfile
├── go.mod
├── go.sum
├── LICENSE
├── main.go
├── README.ja.md
├── README.md
├── smithery.yaml
└── tools_list.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | mcp-server-kintone
2 | mcp-server-kintone.exe
3 | 
```

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
1 | Dockerfile
2 | mcp-server-kintone
3 | mcp-server-kintone.exe
4 | 
```

--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------

```yaml
 1 | before:
 2 |   hooks:
 3 |     - go mod tidy
 4 | 
 5 | builds:
 6 |   - id: with-upx
 7 |     env:
 8 |       - CGO_ENABLED=0
 9 |     goos:
10 |       - linux
11 |     flags:
12 |       - -trimpath
13 |     ldflags:
14 |       - '-s -w'
15 |       - '-X main.Version={{ .Version }}'
16 |       - '-X main.Commit={{ .ShortCommit }}'
17 |     hooks:
18 |       post: 'upx-ucl --lzma {{ .Path }}'
19 |   - id: without-upx
20 |     env:
21 |       - CGO_ENABLED=0
22 |     goos:
23 |       - windows
24 |       - darwin
25 |     flags:
26 |       - -trimpath
27 |     ldflags:
28 |       - '-s -w'
29 |       - '-X main.Version={{ .Version }}'
30 |       - '-X main.Commit={{ .ShortCommit }}'
31 | 
32 | archives:
33 |   - format: tar.gz
34 |     name_template: >-
35 |       {{- .ProjectName -}}
36 |       _
37 |       {{- .Version -}}
38 |       _
39 |       {{- .Os -}}
40 |       _
41 |       {{- if eq .Arch "386" -}}
42 |         i386
43 |       {{- else if eq .Arch "amd64" -}}
44 |         x86_64
45 |       {{- else -}}
46 |         {{- .Arch -}}
47 |       {{- end -}}
48 |     format_overrides:
49 |       - goos: windows
50 |         format: zip
51 |     files: [none*]
52 | 
53 | changelog:
54 |   filters:
55 |     exclude:
56 |       - '^chore'
57 |       - '^docs'
58 |       - '^test'
59 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM golang:latest AS builder
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | COPY go.mod ./
 6 | 
 7 | RUN go mod download
 8 | 
 9 | COPY . .
10 | 
11 | RUN CGO_ENABLED=0 go build -o mcp-server-kintone
12 | 
13 | 
14 | ARG BASE_IMAGE
15 | FROM ${BASE_IMAGE:-alpine:latest}
16 | 
17 | COPY --from=builder /app/mcp-server-kintone /
18 | 
19 | ENTRYPOINT ["/mcp-server-kintone"]
20 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 | 
 6 | jobs:
 7 |   test:
 8 |     name: Test
 9 |     runs-on: ${{ matrix.os }}
10 |     strategy:
11 |       matrix:
12 |         os: [ubuntu-latest, macos-latest, windows-latest]
13 |     steps:
14 |       - uses: actions/setup-go@v3
15 |         with:
16 |           go-version: 1.23.x
17 |       - uses: actions/checkout@v3
18 |       - name: Test
19 |         run: go test -race ./...
20 | 
21 |   analyze:
22 |     name: CodeQL
23 |     runs-on: ubuntu-latest
24 |     steps:
25 |       - uses: actions/checkout@v3
26 |       - uses: github/codeql-action/init@v2
27 |         with:
28 |           languages: go
29 |       - uses: github/codeql-action/analyze@v2
30 | 
31 |   release:
32 |     name: Release
33 |     needs: [test, analyze]
34 |     if: "contains(github.ref, 'tags/v')"
35 |     runs-on: ubuntu-latest
36 |     steps:
37 |       - uses: actions/setup-go@v3
38 |         with:
39 |           go-version: 1.23.x
40 |       - uses: actions/checkout@v3
41 |         with:
42 |           fetch-depth: 0
43 |       - name: Install upx-ucl
44 |         run: sudo apt install upx-ucl -y
45 |       - uses: goreleaser/goreleaser-action@v2
46 |         with:
47 |           version: latest
48 |           args: release --clean
49 |         env:
50 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     type: object
 7 |     anyOf:
 8 |       - required: [url, username, password]
 9 |       - required: [url, token]
10 |     properties:
11 |       url:
12 |         type: string
13 |         description: The URL of the kintone domain. e.g. https://example.kintone.com
14 |       username:
15 |         type: string
16 |         description: The username for kintone authentication. This option and the password option are required if the token option is not provided.
17 |       password:
18 |         type: string
19 |         description: The password for kintone authentication. This option and the username option are required if the token option is not provided.
20 |       token:
21 |         type: string
22 |         description: The API token for kintone authentication. This option is required if the username and password options are not provided.
23 |       allowApps:
24 |         type: string
25 |         description: A comma-separated list of app IDs to allow. If not provided, all apps are allowed.
26 |       denyApps:
27 |         type: string
28 |         description: A comma-separated list of app IDs to deny. If not provided, no apps are denied.
29 | 
30 |   commandFunction: |
31 |     config => ({
32 |       command: './mcp-server-kintone',
33 |       env: {
34 |         KINTONE_BASE_URL: config.url,
35 |         KINTONE_USERNAME: config.username,
36 |         KINTONE_PASSWORD: config.password,
37 |         KINTONE_API_TOKEN: config.token,
38 |         KINTONE_ALLOW_APPS: config.allowApps,
39 |         KINTONE_DENY_APPS: config.denyApps,
40 |       },
41 |     })
42 | 
```

--------------------------------------------------------------------------------
/.github/workflows/docker-publish.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Docker
  2 | 
  3 | # This workflow uses actions that are not certified by GitHub.
  4 | # They are provided by a third-party and are governed by
  5 | # separate terms of service, privacy policy, and support
  6 | # documentation.
  7 | 
  8 | on:
  9 |   schedule:
 10 |     - cron: '18 11 * * *'
 11 |   push:
 12 |     branches: [ "main" ]
 13 |     # Publish semver tags as releases.
 14 |     tags: [ 'v*.*.*' ]
 15 |   pull_request:
 16 |     branches: [ "main" ]
 17 | 
 18 | env:
 19 |   # Use docker.io for Docker Hub if empty
 20 |   REGISTRY: ghcr.io
 21 |   # github.repository as <account>/<repo>
 22 |   IMAGE_NAME: ${{ github.repository }}
 23 | 
 24 | 
 25 | jobs:
 26 |   build:
 27 | 
 28 |     runs-on: ubuntu-latest
 29 |     permissions:
 30 |       contents: read
 31 |       packages: write
 32 |       # This is used to complete the identity challenge
 33 |       # with sigstore/fulcio when running outside of PRs.
 34 |       id-token: write
 35 | 
 36 |     steps:
 37 |       - name: Checkout repository
 38 |         uses: actions/checkout@v4
 39 | 
 40 |       # Install the cosign tool except on PR
 41 |       # https://github.com/sigstore/cosign-installer
 42 |       - name: Install cosign
 43 |         if: github.event_name != 'pull_request'
 44 |         uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
 45 |         with:
 46 |           cosign-release: 'v2.2.4'
 47 | 
 48 |       # Set up BuildKit Docker container builder to be able to build
 49 |       # multi-platform images and export cache
 50 |       # https://github.com/docker/setup-buildx-action
 51 |       - name: Set up Docker Buildx
 52 |         uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
 53 | 
 54 |       # Login against a Docker registry except on PR
 55 |       # https://github.com/docker/login-action
 56 |       - name: Log into registry ${{ env.REGISTRY }}
 57 |         if: github.event_name != 'pull_request'
 58 |         uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
 59 |         with:
 60 |           registry: ${{ env.REGISTRY }}
 61 |           username: ${{ github.actor }}
 62 |           password: ${{ secrets.GITHUB_TOKEN }}
 63 | 
 64 |       # Extract metadata (tags, labels) for Docker
 65 |       # https://github.com/docker/metadata-action
 66 |       - name: Extract Docker metadata
 67 |         id: meta
 68 |         uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
 69 |         with:
 70 |           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
 71 | 
 72 |       # Build and push Docker image with Buildx (don't push on PR)
 73 |       # https://github.com/docker/build-push-action
 74 |       - name: Build and push Docker image
 75 |         id: build-and-push
 76 |         uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
 77 |         with:
 78 |           context: .
 79 |           push: ${{ github.event_name != 'pull_request' }}
 80 |           tags: ${{ steps.meta.outputs.tags }}
 81 |           labels: ${{ steps.meta.outputs.labels }}
 82 |           cache-from: type=gha
 83 |           cache-to: type=gha,mode=max
 84 |           build-args: |
 85 |             BASE_IMAGE=scratch
 86 | 
 87 |       # Sign the resulting Docker image digest except on PRs.
 88 |       # This will only write to the public Rekor transparency log when the Docker
 89 |       # repository is public to avoid leaking data.  If you would like to publish
 90 |       # transparency data even for private images, pass --force to cosign below.
 91 |       # https://github.com/sigstore/cosign
 92 |       - name: Sign the published Docker image
 93 |         if: ${{ github.event_name != 'pull_request' }}
 94 |         env:
 95 |           # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
 96 |           TAGS: ${{ steps.meta.outputs.tags }}
 97 |           DIGEST: ${{ steps.build-and-push.outputs.digest }}
 98 |         # This step uses the identity token to provision an ephemeral certificate
 99 |         # against the sigstore community Fulcio instance.
100 |         run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
101 | 
```

--------------------------------------------------------------------------------
/tools_list.json:
--------------------------------------------------------------------------------

```json
  1 | {{ define "kintoneRecordProperties" }}
  2 | {
  3 |   "properties": {
  4 |     "value": {
  5 |       "anyOf": [
  6 |         {
  7 |           "description": "Usual values for text, number, etc.",
  8 |           "type": "string"
  9 |         },
 10 |         {
 11 |           "description": "Values for checkbox.",
 12 |           "items": {
 13 |             "type": "string"
 14 |           },
 15 |           "type": "array"
 16 |         },
 17 |         {
 18 |           "description": "Values for file attachment.",
 19 |           "items": {
 20 |             "properties": {
 21 |               "contentType": {
 22 |                 "description": "The content type of the file.",
 23 |                 "type": "string"
 24 |               },
 25 |               "fileKey": {
 26 |                 "description": "The file key. You can get the file key to upload a file by using 'uploadAttachmentFile' tool. The file can donwload by using 'downloadAttachmentFile' tool.",
 27 |                 "type": "string"
 28 |               },
 29 |               "name": {
 30 |                 "description": "The file name.",
 31 |                 "type": "string"
 32 |               }
 33 |             },
 34 |             "type": "object"
 35 |           },
 36 |           "type": "array"
 37 |         },
 38 |         {
 39 |           "description": "Values for table.",
 40 |           "properties": {
 41 |             "value": {
 42 |               "items": {
 43 |                 "properties": {
 44 |                   "value": {
 45 |                     "additionalProperties": {
 46 |                       "properties": {
 47 |                         "value": {}
 48 |                       },
 49 |                       "required": [
 50 |                         "value"
 51 |                       ],
 52 |                       "type": "object"
 53 |                     },
 54 |                     "type": "object"
 55 |                   }
 56 |                 },
 57 |                 "required": [
 58 |                   "value"
 59 |                 ],
 60 |                 "type": "object"
 61 |               },
 62 |               "type": "array"
 63 |             }
 64 |           },
 65 |           "required": [
 66 |             "value"
 67 |           ],
 68 |           "type": "object"
 69 |         }
 70 |       ]
 71 |     }
 72 |   },
 73 |   "required": [
 74 |     "value"
 75 |   ],
 76 |   "type": "object"
 77 | }
 78 | {{ end }}
 79 | 
 80 | {
 81 |   "tools": [
 82 |     {
 83 |       "name": "listApps",
 84 |       "description": "List all applications made on kintone. Response includes the app ID, name, and description.",
 85 |       "inputSchema": {
 86 |         "properties": {
 87 |           "offset": {
 88 |             "description": "The offset of apps to read. Default is 0.",
 89 |             "type": "number"
 90 |           },
 91 |           "limit": {
 92 |             "description": "The maximum number of apps to read. Default is 100, maximum is 100. The result might be different from the limit because of the permission.",
 93 |             "type": "number"
 94 |           },
 95 |           "name": {
 96 |             "description": "The name or a part of name of the apps to search. Highly recommended to use this parameter to find the app you want to use.",
 97 |             "type": "string"
 98 |           }
 99 |         },
100 |         "type": "object"
101 |       },
102 |       "annotations": {
103 |         "title": "List kintone apps",
104 |         "readOnlyHint": true,
105 |         "openWorldHint": true
106 |       }
107 |     },
108 |     {
109 |       "name": "readAppInfo",
110 |       "description": "Get information about the specified app. Response includes the app ID, name, description, and schema.",
111 |       "inputSchema": {
112 |         "properties": {
113 |           "appID": {
114 |             "description": "The app ID to get information from.",
115 |             "type": "string"
116 |           }
117 |         },
118 |         "required": [
119 |           "appID"
120 |         ],
121 |         "type": "object"
122 |       },
123 |       "annotations": {
124 |         "title": "Read kintone app information",
125 |         "readOnlyHint": true,
126 |         "openWorldHint": true
127 |       }
128 |     },
129 |     {
130 |       "name": "createRecord",
131 |       "description": "Create a new record in the specified app. Before use this tool, you better to know the schema of the app by using 'readAppInfo' tool.",
132 |       "inputSchema": {
133 |         "properties": {
134 |           "appID": {
135 |             "description": "The app ID to create a record in.",
136 |             "type": "string"
137 |           },
138 |           "record": {
139 |             "additionalProperties": {{ template "kintoneRecordProperties" }},
140 |             "description": "The record data to create. Record data format is the same as kintone's record data format. For example, {\"field1\": {\"value\": \"value1\"}, \"field2\": {\"value\": \"value2\"}, \"field3\": {\"value\": \"value3\"}}.",
141 |             "type": "object"
142 |           }
143 |         },
144 |         "required": [
145 |           "appID",
146 |           "record"
147 |         ],
148 |         "type": "object"
149 |       },
150 |       "annotations": {
151 |         "title": "Create a kintone record",
152 |         "readOnlyHint": false,
153 |         "destructiveHint": false,
154 |         "idempotentHint": false,
155 |         "openWorldHint": true
156 |       }
157 |     },
158 |     {
159 |       "name": "readRecords",
160 |       "description": "Read records from the specified app. Response includes the record ID and record data. Before search records using this tool, you better to know the schema of the app by using 'readAppInfo' tool.",
161 |       "inputSchema": {
162 |         "properties": {
163 |           "appID": {
164 |             "description": "The app ID to read records from.",
165 |             "type": "string"
166 |           },
167 |           "fields": {
168 |             "description": "The field codes to include in the response. Default is all fields.",
169 |             "items": {
170 |               "type": "string"
171 |             },
172 |             "type": "array"
173 |           },
174 |           "limit": {
175 |             "description": "The maximum number of records to read. Default is 10, maximum is 500.",
176 |             "type": "number"
177 |           },
178 |           "offset": {
179 |             "description": "The offset of records to read. Default is 0, maximum is 10,000.",
180 |             "type": "number"
181 |           },
182 |           "query": {
183 |             "description": "The query to filter records. Query format is the same as kintone's query format. For example, 'field1 = \"value1\" and (field2 like \"value2\"' or field3 not in (\"value3.1\",\"value3.2\")) and date > \"2006-01-02\"'.",
184 |             "type": "string"
185 |           }
186 |         },
187 |         "required": [
188 |           "appID"
189 |         ],
190 |         "type": "object"
191 |       },
192 |       "annotations": {
193 |         "title": "Read kintone records",
194 |         "readOnlyHint": true,
195 |         "openWorldHint": true
196 |       }
197 |     },
198 |     {
199 |       "name": "updateRecord",
200 |       "description": "Update the specified record in the specified app. Before use this tool, you better to know the schema of the app by using 'readAppInfo' tool and check which record to update by using 'readRecords' tool.",
201 |       "inputSchema": {
202 |         "properties": {
203 |           "appID": {
204 |             "description": "The app ID to update a record in.",
205 |             "type": "string"
206 |           },
207 |           "record": {
208 |             "additionalProperties": {{ template "kintoneRecordProperties" }},
209 |             "description": "The record data to update. Record data format is the same as kintone's record data format. For example, {\"field1\": {\"value\": \"value1\"}, \"field2\": {\"value\": \"value2\"}, \"field3\": {\"value\": \"value3\"}}. Omits the field that you don't want to update.",
210 |             "type": "object"
211 |           },
212 |           "recordID": {
213 |             "description": "The record ID to update.",
214 |             "type": "string"
215 |           }
216 |         },
217 |         "required": [
218 |           "appID",
219 |           "recordID",
220 |           "record"
221 |         ],
222 |         "type": "object"
223 |       },
224 |       "annotations": {
225 |         "title": "Update a kintone record",
226 |         "readOnlyHint": false,
227 |         "destructiveHint": true,
228 |         "idempotentHint": true,
229 |         "openWorldHint": true
230 |       }
231 |     },
232 |     {
233 |       "name": "deleteRecord",
234 |       "description": "Delete the specified record in the specified app. Before use this tool, you should check which record to delete by using 'readRecords' tool. This operation is unrecoverable, so make sure that the user really want to delete the record.",
235 |       "inputSchema": {
236 |         "properties": {
237 |           "appID": {
238 |             "description": "The app ID to delete a record from.",
239 |             "type": "string"
240 |           },
241 |           "recordID": {
242 |             "description": "The record ID to delete.",
243 |             "type": "string"
244 |           }
245 |         },
246 |         "required": [
247 |           "appID",
248 |           "recordID"
249 |         ],
250 |         "type": "object"
251 |       },
252 |       "annotations": {
253 |         "title": "Delete a kintone records",
254 |         "readOnlyHint": false,
255 |         "destructiveHint": true,
256 |         "idempotentHint": true,
257 |         "openWorldHint": true
258 |       }
259 |     },
260 |     {
261 |       "name": "downloadAttachmentFile",
262 |       "description": "Download the specified attachment file. Before use this tool, you should check file key by using 'readRecords' tool.",
263 |       "inputSchema": {
264 |         "properties": {
265 |           "fileKey": {
266 |             "description": "The file key to download.",
267 |             "type": "string"
268 |           }
269 |         },
270 |         "required": [
271 |           "fileKey"
272 |         ],
273 |         "type": "object"
274 |       },
275 |       "annotations": {
276 |         "title": "Download a file from kintone",
277 |         "readOnlyHint": false,
278 |         "destructiveHint": false,
279 |         "idempotentHint": false,
280 |         "openWorldHint": true
281 |       }
282 |     },
283 |     {
284 |       "name": "uploadAttachmentFile",
285 |       "description": "Upload a new attachment file to the specified app. The response includes a file key that you can use for creating or updating records.",
286 |       "inputSchema": {
287 |         "description": "The file to upload. You can specify the file by path or content.",
288 |         "properties": {
289 |           "path": {
290 |             "description": "The path of the file to upload. Required if `content` is not specified.",
291 |             "type": "string"
292 |           },
293 |           "content": {
294 |             "description": "The content of the file to upload. Required if `path` is not specified.",
295 |             "type": "string"
296 |           },
297 |           "name": {
298 |             "description": "The file name for the `content`. This is only used when `content` is specified.",
299 |             "type": "string"
300 |           },
301 |           "base64": {
302 |             "description": "The `content` is base64 encoded or not. Default is false. This is only used when `content` is specified.",
303 |             "type": "boolean"
304 |           }
305 |         },
306 |         "type": "object"
307 |       },
308 |       "annotations": {
309 |         "title": "Upload a file to kintone",
310 |         "readOnlyHint": false,
311 |         "destructiveHint": false,
312 |         "idempotentHint": true,
313 |         "openWorldHint": true
314 |       }
315 |     },
316 |     {
317 |       "name": "readRecordComments",
318 |       "description": "Read comments on the specified record in the specified app.",
319 |       "inputSchema": {
320 |         "properties": {
321 |           "appID": {
322 |             "description": "The app ID to read comments from.",
323 |             "type": "string"
324 |           },
325 |           "limit": {
326 |             "description": "The maximum number of comments to read. Default is 10, maximum is 10.",
327 |             "type": "number"
328 |           },
329 |           "offset": {
330 |             "description": "The offset of comments to read. Default is 0.",
331 |             "type": "number"
332 |           },
333 |           "order": {
334 |             "description": "The order of comments. Default is 'desc'.",
335 |             "type": "string"
336 |           },
337 |           "recordID": {
338 |             "description": "The record ID to read comments from.",
339 |             "type": "string"
340 |           }
341 |         },
342 |         "required": [
343 |           "appID",
344 |           "recordID"
345 |         ],
346 |         "type": "object"
347 |       },
348 |       "annotations": {
349 |         "title": "Read kintone record's comments",
350 |         "readOnlyHint": true,
351 |         "openWorldHint": true
352 |       }
353 |     },
354 |     {
355 |       "name": "createRecordComment",
356 |       "description": "Create a new comment on the specified record in the specified app.",
357 |       "inputSchema": {
358 |         "properties": {
359 |           "appID": {
360 |             "description": "The app ID to create a comment in.",
361 |             "type": "string"
362 |           },
363 |           "comment": {
364 |             "properties": {
365 |               "mentions": {
366 |                 "description": "The mention targets of the comment. The target can be a user, a group, or a organization.",
367 |                 "items": {
368 |                   "properties": {
369 |                     "code": {
370 |                       "description": "The code of the mention target. You can get the code by other records or comments.",
371 |                       "type": "string"
372 |                     },
373 |                     "type": {
374 |                       "description": "The type of the mention target. Default is 'USER'.",
375 |                       "enum": [
376 |                         "USER",
377 |                         "GROUP",
378 |                         "ORGANIZATION"
379 |                       ],
380 |                       "type": "string"
381 |                     }
382 |                   },
383 |                   "required": [
384 |                     "code"
385 |                   ],
386 |                   "type": "object"
387 |                 },
388 |                 "type": "array"
389 |               },
390 |               "text": {
391 |                 "description": "The text of the comment.",
392 |                 "type": "string"
393 |               }
394 |             },
395 |             "required": [
396 |               "text"
397 |             ],
398 |             "type": "object"
399 |           },
400 |           "recordID": {
401 |             "description": "The record ID to create a comment on.",
402 |             "type": "string"
403 |           }
404 |         },
405 |         "required": [
406 |           "appID",
407 |           "recordID",
408 |           "comment"
409 |         ],
410 |         "type": "object"
411 |       },
412 |       "annotations": {
413 |         "title": "Post a comment to kintone record",
414 |         "readOnlyHint": false,
415 |         "destructiveHint": false,
416 |         "idempotentHint": false,
417 |         "openWorldHint": true
418 |       }
419 |     },
420 |     {
421 |       "name": "updateProcessManagementAssignee",
422 |       "description": "Update the assignee of process management of the specified record in the specified app.",
423 |       "inputSchema": {
424 |         "properties": {
425 |           "appID": {
426 |             "description": "The app ID to update the assignee.",
427 |             "type": "string"
428 |           },
429 |           "recordID": {
430 |             "description": "The record ID to update the assignee.",
431 |             "type": "string"
432 |           },
433 |           "assignees": {
434 |             "description": "The assignee of the record.",
435 |             "type": "array",
436 |             "items": {
437 |               "type": "string",
438 |               "description": "The code of the assignee user."
439 |             }
440 |           }
441 |         },
442 |         "required": [
443 |           "appID",
444 |           "recordID",
445 |           "assignees"
446 |         ],
447 |         "type": "object"
448 |       },
449 |       "annotations": {
450 |         "title": "Update kintone record's assignee",
451 |         "readOnlyHint": false,
452 |         "destructiveHint": true,
453 |         "idempotentHint": true,
454 |         "openWorldHint": true
455 |       }
456 |     },
457 |     {
458 |       "name": "executeProcessManagementAction",
459 |       "description": "Execute the specified action of process management of the specified record in the specified app.",
460 |       "inputSchema": {
461 |         "properties": {
462 |           "appID": {
463 |             "description": "The app ID to execute the action.",
464 |             "type": "string"
465 |           },
466 |           "recordID": {
467 |             "description": "The record ID to execute the action.",
468 |             "type": "string"
469 |           },
470 |           "action": {
471 |             "description": "The action to execute.",
472 |             "type": "string"
473 |           },
474 |           "assignee": {
475 |             "description": "The next assignee of the record.",
476 |             "type": "string"
477 |           }
478 |         },
479 |         "required": [
480 |           "appID",
481 |           "recordID",
482 |           "action"
483 |         ],
484 |         "type": "object"
485 |       },
486 |       "annotations": {
487 |         "title": "Execute kintone record's process management action",
488 |         "readOnlyHint": false,
489 |         "destructiveHint": true,
490 |         "idempotentHint": false,
491 |         "openWorldHint": true
492 |       }
493 |     }
494 |   ]
495 | }
496 | {{/* vim: set ft=json et ts=2 sw=2: */}}
497 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
   1 | package main
   2 | 
   3 | import (
   4 | 	"bytes"
   5 | 	"context"
   6 | 	_ "embed"
   7 | 	"encoding/base64"
   8 | 	"encoding/json"
   9 | 	"errors"
  10 | 	"fmt"
  11 | 	"io"
  12 | 	"mime"
  13 | 	"mime/multipart"
  14 | 	"net/http"
  15 | 	"net/url"
  16 | 	"os"
  17 | 	"path/filepath"
  18 | 	"slices"
  19 | 	"strconv"
  20 | 	"strings"
  21 | 	"text/template"
  22 | 
  23 | 	"github.com/macrat/go-jsonrpc2"
  24 | )
  25 | 
  26 | var (
  27 | 	Version = "UNKNOWN"
  28 | 	Commit  = "HEAD"
  29 | )
  30 | 
  31 | type JsonMap map[string]any
  32 | 
  33 | type ServerInfo struct {
  34 | 	Name    string `json:"name"`
  35 | 	Version string `json:"version"`
  36 | }
  37 | 
  38 | type InitializeRequest struct {
  39 | 	ProtocolVersion string `json:"protocolVersion"`
  40 | }
  41 | 
  42 | type InitializeResult struct {
  43 | 	ProtocolVersion string     `json:"protocolVersion"`
  44 | 	Capabilities    JsonMap    `json:"capabilities"`
  45 | 	ServerInfo      ServerInfo `json:"serverInfo"`
  46 | 	Instructions    string     `json:"instructions"`
  47 | }
  48 | 
  49 | type Content struct {
  50 | 	Type     string `json:"type"`
  51 | 	Text     string `json:"text,omitempty"`
  52 | 	Data     string `json:"data,omitempty"`
  53 | 	MimeType string `json:"mimeType,omitempty"`
  54 | }
  55 | 
  56 | func JSONContent(v any) ([]Content, error) {
  57 | 	bs, err := json.MarshalIndent(v, "", "  ")
  58 | 	if err != nil {
  59 | 		return nil, err
  60 | 	}
  61 | 	return []Content{{Type: "text", Text: string(bs)}}, nil
  62 | }
  63 | 
  64 | type ToolInfo struct {
  65 | 	Name        string  `json:"name"`
  66 | 	Description string  `json:"description,omitempty"`
  67 | 	InputSchema JsonMap `json:"inputSchema"`
  68 | }
  69 | 
  70 | type ToolsListResult struct {
  71 | 	Tools []ToolInfo `json:"tools"`
  72 | }
  73 | 
  74 | type ToolsCallRequest struct {
  75 | 	Name      string          `json:"name"`
  76 | 	Arguments json.RawMessage `json:"arguments"`
  77 | }
  78 | 
  79 | type ToolsCallResult struct {
  80 | 	Content []Content `json:"content"`
  81 | 	IsError bool      `json:"isError"`
  82 | }
  83 | 
  84 | func UnmarshalParams[T any](data []byte, target *T) error {
  85 | 	err := json.Unmarshal(data, target)
  86 | 	if err != nil {
  87 | 		return jsonrpc2.Error{
  88 | 			Code:    jsonrpc2.InvalidParamsCode,
  89 | 			Message: fmt.Sprintf("Failed to parse parameters: %v", err),
  90 | 		}
  91 | 	}
  92 | 	return nil
  93 | }
  94 | 
  95 | type ProcessManagement struct {
  96 | 	Enable  bool               `json:"enable"`
  97 | 	States  map[string]JsonMap `json:"states,omitempty"`
  98 | 	Actions []JsonMap          `json:"actions,omitempty"`
  99 | }
 100 | 
 101 | type KintoneAppDetail struct {
 102 | 	AppID             string            `json:"appID"`
 103 | 	Name              string            `json:"name"`
 104 | 	Description       string            `json:"description,omitempty"`
 105 | 	Properties        JsonMap           `json:"properties,omitempty"`
 106 | 	CreatedAt         string            `json:"createdAt"`
 107 | 	ModifiedAt        string            `json:"modifiedAt"`
 108 | 	ProcessManagement ProcessManagement `json:"processManagement,omitempty"`
 109 | }
 110 | 
 111 | type KintoneHandlers struct {
 112 | 	URL   *url.URL
 113 | 	Auth  string
 114 | 	Token string
 115 | 	Allow []string
 116 | 	Deny  []string
 117 | }
 118 | 
 119 | func NewKintoneHandlersFromEnv() (*KintoneHandlers, error) {
 120 | 	var handlers KintoneHandlers
 121 | 	errs := []error{errors.New("Error:")}
 122 | 
 123 | 	username := Getenv("KINTONE_USERNAME", "")
 124 | 	password := Getenv("KINTONE_PASSWORD", "")
 125 | 	tokens := Getenv("KINTONE_API_TOKEN", "")
 126 | 	if (username == "" || password == "") && tokens == "" {
 127 | 		errs = append(errs, errors.New("- Either KINTONE_USERNAME/KINTONE_PASSWORD or KINTONE_API_TOKEN must be provided"))
 128 | 	}
 129 | 	if username != "" && password != "" {
 130 | 		handlers.Auth = base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", username, password))
 131 | 	}
 132 | 	handlers.Token = tokens
 133 | 
 134 | 	baseURL := Getenv("KINTONE_BASE_URL", "")
 135 | 	if baseURL == "" {
 136 | 		errs = append(errs, errors.New("- KINTONE_BASE_URL must be provided"))
 137 | 	} else if u, err := url.Parse(baseURL); err != nil {
 138 | 		errs = append(errs, fmt.Errorf("- Failed to parse KINTONE_BASE_URL: %s", err))
 139 | 	} else {
 140 | 		handlers.URL = u
 141 | 	}
 142 | 
 143 | 	handlers.Allow = GetenvList("KINTONE_ALLOW_APPS")
 144 | 	handlers.Deny = GetenvList("KINTONE_DENY_APPS")
 145 | 
 146 | 	if len(errs) > 1 {
 147 | 		return nil, errors.Join(errs...)
 148 | 	}
 149 | 
 150 | 	return &handlers, nil
 151 | }
 152 | 
 153 | type Query map[string]string
 154 | 
 155 | func (q Query) Encode() string {
 156 | 	values := make(url.Values)
 157 | 	for k, v := range q {
 158 | 		values.Set(k, v)
 159 | 	}
 160 | 	return values.Encode()
 161 | }
 162 | 
 163 | func (h *KintoneHandlers) SendHTTP(ctx context.Context, method, path string, query Query, body io.Reader, contentType string) (*http.Response, error) {
 164 | 	endpoint := h.URL.JoinPath(path)
 165 | 	endpoint.RawQuery = query.Encode()
 166 | 
 167 | 	req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
 168 | 	if err != nil {
 169 | 		return nil, jsonrpc2.Error{
 170 | 			Code:    jsonrpc2.InternalErrorCode,
 171 | 			Message: fmt.Sprintf("Failed to create HTTP request: %v", err),
 172 | 		}
 173 | 	}
 174 | 
 175 | 	if h.Auth != "" {
 176 | 		req.Header.Set("X-Cybozu-Authorization", h.Auth)
 177 | 	}
 178 | 	if h.Token != "" {
 179 | 		req.Header.Set("X-Cybozu-API-Token", h.Token)
 180 | 	}
 181 | 	if body != nil {
 182 | 		req.Header.Set("Content-Type", contentType)
 183 | 	}
 184 | 
 185 | 	res, err := http.DefaultClient.Do(req)
 186 | 	if err != nil {
 187 | 		return nil, jsonrpc2.Error{
 188 | 			Code:    jsonrpc2.InternalErrorCode,
 189 | 			Message: fmt.Sprintf("Failed to send HTTP request to kintone server: %v", err),
 190 | 		}
 191 | 	}
 192 | 
 193 | 	if res.StatusCode != http.StatusOK {
 194 | 		msg, _ := io.ReadAll(res.Body)
 195 | 		res.Body.Close()
 196 | 		return nil, jsonrpc2.Error{
 197 | 			Code:    jsonrpc2.InternalErrorCode,
 198 | 			Message: fmt.Sprintf("kintone server returned an error: %s\n%s", res.Status, msg),
 199 | 		}
 200 | 	}
 201 | 
 202 | 	return res, nil
 203 | }
 204 | 
 205 | func (h *KintoneHandlers) FetchHTTPWithReader(ctx context.Context, method, path string, query Query, body io.Reader, contentType string, result any) error {
 206 | 	res, err := h.SendHTTP(ctx, method, path, query, body, contentType)
 207 | 	if err != nil {
 208 | 		return err
 209 | 	}
 210 | 	defer res.Body.Close()
 211 | 
 212 | 	if result != nil {
 213 | 		if err := json.NewDecoder(res.Body).Decode(result); err != nil {
 214 | 			return jsonrpc2.Error{
 215 | 				Code:    jsonrpc2.InternalErrorCode,
 216 | 				Message: fmt.Sprintf("Failed to parse kintone server's response: %v", err),
 217 | 			}
 218 | 		}
 219 | 	}
 220 | 
 221 | 	return nil
 222 | }
 223 | 
 224 | func (h *KintoneHandlers) FetchHTTPWithJSON(ctx context.Context, method, path string, query Query, body, result any) error {
 225 | 	var reqBody io.Reader
 226 | 	if body != nil {
 227 | 		bs, err := json.Marshal(body)
 228 | 		if err != nil {
 229 | 			return jsonrpc2.Error{
 230 | 				Code:    jsonrpc2.InternalErrorCode,
 231 | 				Message: fmt.Sprintf("Failed to prepare request body for kintone server: %v", err),
 232 | 			}
 233 | 		}
 234 | 		reqBody = bytes.NewReader(bs)
 235 | 	}
 236 | 
 237 | 	return h.FetchHTTPWithReader(ctx, method, path, query, reqBody, "application/json", result)
 238 | }
 239 | 
 240 | func (h *KintoneHandlers) InitializeHandler(ctx context.Context, params InitializeRequest) (InitializeResult, error) {
 241 | 	version := "2025-03-26"
 242 | 	if params.ProtocolVersion < version {
 243 | 		version = params.ProtocolVersion
 244 | 	}
 245 | 
 246 | 	return InitializeResult{
 247 | 		ProtocolVersion: version,
 248 | 		Capabilities: JsonMap{
 249 | 			"tools": JsonMap{},
 250 | 		},
 251 | 		ServerInfo: ServerInfo{
 252 | 			Name:    "Kintone Server",
 253 | 			Version: fmt.Sprintf("%s (%s)", Version, Commit),
 254 | 		},
 255 | 		Instructions: fmt.Sprintf("kintone is a database service to store and manage enterprise data. You can use this server to interact with kintone."),
 256 | 	}, nil
 257 | }
 258 | 
 259 | //go:embed tools_list.json
 260 | var toolsListTmplStr string
 261 | 
 262 | var toolsList ToolsListResult
 263 | 
 264 | func init() {
 265 | 	tmpl, err := template.New("tools_list").Parse(toolsListTmplStr)
 266 | 	if err != nil {
 267 | 		panic(fmt.Sprintf("Failed to parse tools list template: %v", err))
 268 | 	}
 269 | 
 270 | 	var buf bytes.Buffer
 271 | 	if err := tmpl.Execute(&buf, nil); err != nil {
 272 | 		panic(fmt.Sprintf("Failed to render tools list template: %v", err))
 273 | 	}
 274 | 
 275 | 	if err := json.Unmarshal(buf.Bytes(), &toolsList); err != nil {
 276 | 		panic(fmt.Sprintf("Failed to parse tools list JSON: %v", err))
 277 | 	}
 278 | }
 279 | 
 280 | func (h *KintoneHandlers) ToolsList(ctx context.Context, params any) (ToolsListResult, error) {
 281 | 	return toolsList, nil
 282 | }
 283 | 
 284 | func (h *KintoneHandlers) ToolsCall(ctx context.Context, params ToolsCallRequest) (ToolsCallResult, error) {
 285 | 	var content []Content
 286 | 	var err error
 287 | 
 288 | 	switch params.Name {
 289 | 	case "listApps":
 290 | 		content, err = h.ListApps(ctx, params.Arguments)
 291 | 	case "readAppInfo":
 292 | 		content, err = h.ReadAppInfo(ctx, params.Arguments)
 293 | 	case "createRecord":
 294 | 		content, err = h.CreateRecord(ctx, params.Arguments)
 295 | 	case "readRecords":
 296 | 		content, err = h.ReadRecords(ctx, params.Arguments)
 297 | 	case "updateRecord":
 298 | 		content, err = h.UpdateRecord(ctx, params.Arguments)
 299 | 	case "deleteRecord":
 300 | 		content, err = h.DeleteRecord(ctx, params.Arguments)
 301 | 	case "downloadAttachmentFile":
 302 | 		content, err = h.DownloadAttachmentFile(ctx, params.Arguments)
 303 | 	case "uploadAttachmentFile":
 304 | 		content, err = h.UploadAttachmentFile(ctx, params.Arguments)
 305 | 	case "readRecordComments":
 306 | 		content, err = h.ReadRecordComments(ctx, params.Arguments)
 307 | 	case "createRecordComment":
 308 | 		content, err = h.CreateRecordComment(ctx, params.Arguments)
 309 | 	case "updateProcessManagementAssignee":
 310 | 		content, err = h.UpdateProcessManagementAssignee(ctx, params.Arguments)
 311 | 	case "executeProcessManagementAction":
 312 | 		content, err = h.ExecuteProcessManagementAction(ctx, params.Arguments)
 313 | 	default:
 314 | 		return ToolsCallResult{}, jsonrpc2.Error{
 315 | 			Code:    jsonrpc2.InvalidParamsCode,
 316 | 			Message: fmt.Sprintf("Unknown tool name: %s", params.Name),
 317 | 		}
 318 | 	}
 319 | 
 320 | 	if err != nil {
 321 | 		return ToolsCallResult{}, err
 322 | 	}
 323 | 
 324 | 	return ToolsCallResult{
 325 | 		Content: content,
 326 | 	}, nil
 327 | }
 328 | 
 329 | func (h *KintoneHandlers) checkPermissions(id string) error {
 330 | 	if slices.Contains(h.Deny, id) {
 331 | 		return jsonrpc2.Error{
 332 | 			Code:    jsonrpc2.InvalidParamsCode,
 333 | 			Message: fmt.Sprintf("App ID %s is inaccessible because it is listed in the KINTONE_DENY_APPS environment variable. Please check the MCP server settings.", id),
 334 | 		}
 335 | 	}
 336 | 	if len(h.Allow) > 0 && !slices.Contains(h.Allow, id) {
 337 | 		return jsonrpc2.Error{
 338 | 			Code:    jsonrpc2.InvalidParamsCode,
 339 | 			Message: fmt.Sprintf("App ID %s is inaccessible because it is not listed in the KINTONE_ALLOW_APPS environment variable. Please check the MCP server settings.", id),
 340 | 		}
 341 | 	}
 342 | 
 343 | 	return nil
 344 | }
 345 | 
 346 | func (h *KintoneHandlers) ListApps(ctx context.Context, params json.RawMessage) ([]Content, error) {
 347 | 	var req struct {
 348 | 		Offset int     `json:"offset"`
 349 | 		Limit  *int    `json:"limit"`
 350 | 		Name   *string `json:"name"`
 351 | 	}
 352 | 	if err := UnmarshalParams(params, &req); err != nil {
 353 | 		return nil, err
 354 | 	}
 355 | 	if req.Offset < 0 {
 356 | 		return nil, jsonrpc2.Error{
 357 | 			Code:    jsonrpc2.InvalidParamsCode,
 358 | 			Message: "Offset must be greater than or equal to 0",
 359 | 		}
 360 | 	}
 361 | 	if req.Limit == nil {
 362 | 		limit := 100
 363 | 		req.Limit = &limit
 364 | 	} else if *req.Limit < 1 || *req.Limit > 100 {
 365 | 		return nil, jsonrpc2.Error{
 366 | 			Code:    jsonrpc2.InvalidParamsCode,
 367 | 			Message: "Limit must be between 1 and 100",
 368 | 		}
 369 | 	}
 370 | 
 371 | 	type Res struct {
 372 | 		Apps []KintoneAppDetail `json:"apps"`
 373 | 	}
 374 | 
 375 | 	var httpRes Res
 376 | 	err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/apps.json", nil, req, &httpRes)
 377 | 	if err != nil {
 378 | 		return nil, err
 379 | 	}
 380 | 
 381 | 	apps := make([]KintoneAppDetail, 0, len(httpRes.Apps))
 382 | 	for _, app := range httpRes.Apps {
 383 | 		if err := h.checkPermissions(app.AppID); err == nil {
 384 | 			apps = append(apps, app)
 385 | 		}
 386 | 	}
 387 | 
 388 | 	hasNext := false
 389 | 	var httpRes2 Res
 390 | 	err = h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/apps.json", nil, JsonMap{"offset": req.Offset + len(httpRes.Apps), "limit": 1}, &httpRes2)
 391 | 	if err == nil {
 392 | 		hasNext = len(httpRes2.Apps) > 0
 393 | 	}
 394 | 
 395 | 	return JSONContent(JsonMap{
 396 | 		"apps":    apps,
 397 | 		"hasNext": hasNext,
 398 | 	})
 399 | }
 400 | 
 401 | func (h *KintoneHandlers) ReadAppInfo(ctx context.Context, params json.RawMessage) ([]Content, error) {
 402 | 	var req struct {
 403 | 		AppID string `json:"appID"`
 404 | 	}
 405 | 	if err := UnmarshalParams(params, &req); err != nil {
 406 | 		return nil, err
 407 | 	}
 408 | 	if req.AppID == "" {
 409 | 		return nil, jsonrpc2.Error{
 410 | 			Code:    jsonrpc2.InvalidParamsCode,
 411 | 			Message: "Argument 'appID' is required",
 412 | 		}
 413 | 	}
 414 | 
 415 | 	if err := h.checkPermissions(req.AppID); err != nil {
 416 | 		return nil, err
 417 | 	}
 418 | 
 419 | 	var app KintoneAppDetail
 420 | 	if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/app.json", Query{"id": req.AppID}, nil, &app); err != nil {
 421 | 		return nil, err
 422 | 	}
 423 | 
 424 | 	var fields struct {
 425 | 		Properties JsonMap `json:"properties"`
 426 | 	}
 427 | 	if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/app/form/fields.json", Query{"app": req.AppID}, nil, &fields); err != nil {
 428 | 		return nil, err
 429 | 	}
 430 | 	app.Properties = fields.Properties
 431 | 
 432 | 	var process ProcessManagement
 433 | 	if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/app/status.json", Query{"app": req.AppID}, nil, &process); err != nil {
 434 | 		return nil, err
 435 | 	}
 436 | 	if !process.Enable {
 437 | 		process.States = nil
 438 | 		process.Actions = nil
 439 | 	}
 440 | 	app.ProcessManagement = process
 441 | 
 442 | 	return JSONContent(app)
 443 | }
 444 | 
 445 | func (h *KintoneHandlers) CreateRecord(ctx context.Context, params json.RawMessage) ([]Content, error) {
 446 | 	var req struct {
 447 | 		AppID  string  `json:"appID"`
 448 | 		Record JsonMap `json:"record"`
 449 | 	}
 450 | 	if err := UnmarshalParams(params, &req); err != nil {
 451 | 		return nil, err
 452 | 	}
 453 | 	if req.AppID == "" || req.Record == nil {
 454 | 		return nil, jsonrpc2.Error{
 455 | 			Code:    jsonrpc2.InvalidParamsCode,
 456 | 			Message: "Arguments 'appID' and 'record' are required",
 457 | 		}
 458 | 	}
 459 | 
 460 | 	if err := h.checkPermissions(req.AppID); err != nil {
 461 | 		return nil, err
 462 | 	}
 463 | 
 464 | 	httpReq := JsonMap{
 465 | 		"app":    req.AppID,
 466 | 		"record": req.Record,
 467 | 	}
 468 | 	var record struct {
 469 | 		ID string `json:"id"`
 470 | 	}
 471 | 	if err := h.FetchHTTPWithJSON(ctx, "POST", "/k/v1/record.json", nil, httpReq, &record); err != nil {
 472 | 		return nil, err
 473 | 	}
 474 | 
 475 | 	return JSONContent(JsonMap{
 476 | 		"success":  true,
 477 | 		"recordID": record.ID,
 478 | 	})
 479 | }
 480 | 
 481 | func (h *KintoneHandlers) ReadRecords(ctx context.Context, params json.RawMessage) ([]Content, error) {
 482 | 	var req struct {
 483 | 		AppID  string   `json:"appID"`
 484 | 		Query  string   `json:"query"`
 485 | 		Limit  *int     `json:"limit"`
 486 | 		Fields []string `json:"fields"`
 487 | 		Offset int      `json:"offset"`
 488 | 	}
 489 | 	if err := UnmarshalParams(params, &req); err != nil {
 490 | 		return nil, err
 491 | 	}
 492 | 	if req.AppID == "" {
 493 | 		return nil, jsonrpc2.Error{
 494 | 			Code:    jsonrpc2.InvalidParamsCode,
 495 | 			Message: "Argument 'appID' is required",
 496 | 		}
 497 | 	}
 498 | 
 499 | 	if req.Limit == nil {
 500 | 		limit := 10
 501 | 		req.Limit = &limit
 502 | 	} else if *req.Limit < 1 || *req.Limit > 500 {
 503 | 		return nil, jsonrpc2.Error{
 504 | 			Code:    jsonrpc2.InvalidParamsCode,
 505 | 			Message: "Limit must be between 1 and 500",
 506 | 		}
 507 | 	}
 508 | 
 509 | 	if req.Offset < 0 || req.Offset > 10000 {
 510 | 		return nil, jsonrpc2.Error{
 511 | 			Code:    jsonrpc2.InvalidParamsCode,
 512 | 			Message: "Offset must be between 0 and 10000",
 513 | 		}
 514 | 	}
 515 | 
 516 | 	if err := h.checkPermissions(req.AppID); err != nil {
 517 | 		return nil, err
 518 | 	}
 519 | 
 520 | 	httpReq := JsonMap{
 521 | 		"app":        req.AppID,
 522 | 		"query":      req.Query,
 523 | 		"limit":      *req.Limit,
 524 | 		"offset":     req.Offset,
 525 | 		"fields":     req.Fields,
 526 | 		"totalCount": true,
 527 | 	}
 528 | 
 529 | 	var records JsonMap
 530 | 	if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/records.json", nil, httpReq, &records); err != nil {
 531 | 		return nil, err
 532 | 	}
 533 | 
 534 | 	return JSONContent(records)
 535 | }
 536 | 
 537 | func (h *KintoneHandlers) UpdateRecord(ctx context.Context, params json.RawMessage) ([]Content, error) {
 538 | 	var req struct {
 539 | 		AppID    string `json:"appID"`
 540 | 		RecordID string `json:"recordID"`
 541 | 		Record   any    `json:"record"`
 542 | 	}
 543 | 	if err := UnmarshalParams(params, &req); err != nil {
 544 | 		return nil, err
 545 | 	}
 546 | 	if req.AppID == "" || req.RecordID == "" || req.Record == nil {
 547 | 		return nil, jsonrpc2.Error{
 548 | 			Code:    jsonrpc2.InvalidParamsCode,
 549 | 			Message: "Arguments 'appID', 'recordID', and 'record' are required",
 550 | 		}
 551 | 	}
 552 | 
 553 | 	if err := h.checkPermissions(req.AppID); err != nil {
 554 | 		return nil, err
 555 | 	}
 556 | 
 557 | 	httpReq := JsonMap{
 558 | 		"app":    req.AppID,
 559 | 		"id":     req.RecordID,
 560 | 		"record": req.Record,
 561 | 	}
 562 | 	var result struct {
 563 | 		Revision string `json:"revision"`
 564 | 	}
 565 | 	if err := h.FetchHTTPWithJSON(ctx, "PUT", "/k/v1/record.json", nil, httpReq, &result); err != nil {
 566 | 		return nil, err
 567 | 	}
 568 | 
 569 | 	return JSONContent(JsonMap{
 570 | 		"success":  true,
 571 | 		"revision": result.Revision,
 572 | 	})
 573 | }
 574 | 
 575 | func (h *KintoneHandlers) readSingleRecord(ctx context.Context, appID, recordID string) (JsonMap, error) {
 576 | 	var result struct {
 577 | 		Record JsonMap `json:"record"`
 578 | 	}
 579 | 	err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/record.json", Query{"app": appID, "id": recordID}, nil, &result)
 580 | 
 581 | 	return result.Record, err
 582 | }
 583 | 
 584 | func (h *KintoneHandlers) DeleteRecord(ctx context.Context, params json.RawMessage) ([]Content, error) {
 585 | 	var req struct {
 586 | 		AppID    string `json:"appID"`
 587 | 		RecordID string `json:"recordID"`
 588 | 	}
 589 | 	if err := UnmarshalParams(params, &req); err != nil {
 590 | 		return nil, err
 591 | 	}
 592 | 	if req.AppID == "" || req.RecordID == "" {
 593 | 		return nil, jsonrpc2.Error{
 594 | 			Code:    jsonrpc2.InvalidParamsCode,
 595 | 			Message: "Arguments 'appID' and 'recordID' are required",
 596 | 		}
 597 | 	}
 598 | 
 599 | 	if err := h.checkPermissions(req.AppID); err != nil {
 600 | 		return nil, err
 601 | 	}
 602 | 
 603 | 	deletedRecord, err := h.readSingleRecord(ctx, req.AppID, req.RecordID)
 604 | 	if err != nil {
 605 | 		return nil, err
 606 | 	}
 607 | 
 608 | 	if err := h.FetchHTTPWithJSON(ctx, "DELETE", "/k/v1/records.json", Query{"app": req.AppID, "ids[0]": req.RecordID}, nil, nil); err != nil {
 609 | 		return nil, err
 610 | 	}
 611 | 
 612 | 	result := JsonMap{
 613 | 		"success": true,
 614 | 	}
 615 | 	if deletedRecord != nil {
 616 | 		result["deletedRecord"] = deletedRecord
 617 | 	}
 618 | 	return JSONContent(result)
 619 | }
 620 | 
 621 | func getDownloadDirectory() string {
 622 | 	dir, err := os.UserHomeDir()
 623 | 	if err != nil {
 624 | 		return os.TempDir()
 625 | 	}
 626 | 
 627 | 	for _, d := range []string{"Downloads", "downloads", "Download", "download"} {
 628 | 		d = filepath.Join(dir, d)
 629 | 		if _, err := os.Stat(d); err == nil {
 630 | 			return d
 631 | 		}
 632 | 	}
 633 | 
 634 | 	dir = filepath.Join(dir, "Downloads")
 635 | 	err = os.MkdirAll(dir, 0755)
 636 | 	if err != nil {
 637 | 		return os.TempDir()
 638 | 	}
 639 | 	return dir
 640 | }
 641 | 
 642 | func getDownloadFilePath(fileName string) string {
 643 | 	dir := getDownloadDirectory()
 644 | 
 645 | 	p := filepath.Join(dir, fileName)
 646 | 	if _, err := os.Stat(p); err != nil {
 647 | 		return p
 648 | 	}
 649 | 
 650 | 	ext := filepath.Ext(fileName)
 651 | 	base := strings.TrimSuffix(fileName, ext)
 652 | 
 653 | 	num := 1
 654 | 	if strings.HasSuffix(base, ")") {
 655 | 		if i := strings.LastIndex(base, " ("); i > 0 {
 656 | 			if n, err := strconv.Atoi(base[i+2:]); err == nil {
 657 | 				base = base[:i]
 658 | 				num = n
 659 | 			}
 660 | 		}
 661 | 	}
 662 | 
 663 | 	for {
 664 | 		p = filepath.Join(dir, fmt.Sprintf("%s (%d)%s", base, num, ext))
 665 | 		if _, err := os.Stat(p); err != nil {
 666 | 			return p
 667 | 		}
 668 | 		num++
 669 | 	}
 670 | }
 671 | 
 672 | func (h *KintoneHandlers) DownloadAttachmentFile(ctx context.Context, params json.RawMessage) ([]Content, error) {
 673 | 	var req struct {
 674 | 		FileKey string `json:"fileKey"`
 675 | 	}
 676 | 	if err := UnmarshalParams(params, &req); err != nil {
 677 | 		return nil, err
 678 | 	}
 679 | 	if req.FileKey == "" {
 680 | 		return nil, jsonrpc2.Error{
 681 | 			Code:    jsonrpc2.InvalidParamsCode,
 682 | 			Message: "Argument 'fileKey' is required",
 683 | 		}
 684 | 	}
 685 | 
 686 | 	httpRes, err := h.SendHTTP(ctx, "GET", "/k/v1/file.json", Query{"fileKey": req.FileKey}, nil, "")
 687 | 	if err != nil {
 688 | 		return nil, err
 689 | 	}
 690 | 	defer httpRes.Body.Close()
 691 | 
 692 | 	contentType := httpRes.Header.Get("Content-Type")
 693 | 	if contentType == "" {
 694 | 		contentType = "application/octet-stream"
 695 | 	}
 696 | 
 697 | 	var fileName string
 698 | 
 699 | 	_, ps, err := mime.ParseMediaType(httpRes.Header.Get("Content-Disposition"))
 700 | 	if err == nil {
 701 | 		fileName = ps["filename"]
 702 | 	}
 703 | 
 704 | 	fileName, err = new(mime.WordDecoder).DecodeHeader(fileName)
 705 | 	if err != nil {
 706 | 		fmt.Fprintf(os.Stderr, "Failed to decode filename: %v\n", err)
 707 | 		fileName = ""
 708 | 	}
 709 | 
 710 | 	if fileName == "" {
 711 | 		fileName = req.FileKey
 712 | 
 713 | 		ext, err := mime.ExtensionsByType(contentType)
 714 | 		if err == nil && len(ext) > 0 {
 715 | 			fileName += ext[0]
 716 | 		}
 717 | 	}
 718 | 
 719 | 	outPath := getDownloadFilePath(fileName)
 720 | 	outFile, err := os.Create(outPath)
 721 | 	if err != nil {
 722 | 		return nil, jsonrpc2.Error{
 723 | 			Code:    jsonrpc2.InternalErrorCode,
 724 | 			Message: fmt.Sprintf("Failed to create file for attachment: %v", err),
 725 | 			Data:    JsonMap{"filePath": outPath},
 726 | 		}
 727 | 	}
 728 | 	defer outFile.Close()
 729 | 
 730 | 	var w io.Writer = outFile
 731 | 	var buf *bytes.Buffer
 732 | 	if strings.HasPrefix(contentType, "text/") || strings.HasPrefix(contentType, "image/") {
 733 | 		buf = new(bytes.Buffer)
 734 | 		w = io.MultiWriter(outFile, buf)
 735 | 	}
 736 | 
 737 | 	size, err := io.Copy(w, httpRes.Body)
 738 | 	if err != nil {
 739 | 		outFile.Close()
 740 | 		os.Remove(outPath)
 741 | 		return nil, jsonrpc2.Error{
 742 | 			Code:    jsonrpc2.InternalErrorCode,
 743 | 			Message: fmt.Sprintf("Failed to save attachment file: %s: %v", outPath, err),
 744 | 		}
 745 | 	}
 746 | 
 747 | 	res, err := JSONContent(JsonMap{
 748 | 		"success":  true,
 749 | 		"filePath": outPath,
 750 | 		"size":     size,
 751 | 	})
 752 | 	if err != nil {
 753 | 		return nil, err
 754 | 	}
 755 | 
 756 | 	if strings.HasPrefix(contentType, "text/") {
 757 | 		res = append(res, Content{Type: "text", Text: buf.String()})
 758 | 	} else if strings.HasPrefix(contentType, "image/") {
 759 | 		b64 := base64.StdEncoding.EncodeToString(buf.Bytes())
 760 | 		res = append(res, Content{
 761 | 			Type:     "image",
 762 | 			Data:     b64,
 763 | 			MimeType: contentType,
 764 | 		})
 765 | 	}
 766 | 
 767 | 	return res, nil
 768 | }
 769 | 
 770 | func (h *KintoneHandlers) UploadAttachmentFile(ctx context.Context, params json.RawMessage) ([]Content, error) {
 771 | 	var req struct {
 772 | 		Path    *string `json:"path"`
 773 | 		Name    string  `json:"name"`
 774 | 		Content *string `json:"content"`
 775 | 		Base64  bool    `json:"base64"`
 776 | 	}
 777 | 	if err := UnmarshalParams(params, &req); err != nil {
 778 | 		return nil, err
 779 | 	}
 780 | 
 781 | 	if req.Path == nil && req.Content == nil {
 782 | 		return nil, jsonrpc2.Error{
 783 | 			Code:    jsonrpc2.InvalidParamsCode,
 784 | 			Message: "Arguments 'path' or 'content' is required",
 785 | 		}
 786 | 	}
 787 | 	if req.Path != nil && req.Content != nil {
 788 | 		return nil, jsonrpc2.Error{
 789 | 			Code:    jsonrpc2.InvalidParamsCode,
 790 | 			Message: "Arguments 'path' and 'content' are mutually exclusive",
 791 | 		}
 792 | 	}
 793 | 
 794 | 	var filename string
 795 | 	if req.Path != nil {
 796 | 		filename = filepath.Base(*req.Path)
 797 | 	} else {
 798 | 		filename = req.Name
 799 | 		if filename == "" {
 800 | 			filename = "file"
 801 | 
 802 | 			ext, err := mime.ExtensionsByType(mime.TypeByExtension(filepath.Ext(req.Name)))
 803 | 			if err == nil && len(ext) > 0 {
 804 | 				filename += ext[0]
 805 | 			}
 806 | 		}
 807 | 	}
 808 | 
 809 | 	var body bytes.Buffer
 810 | 	mw := multipart.NewWriter(&body)
 811 | 	part, err := mw.CreateFormFile("file", filename)
 812 | 	if err != nil {
 813 | 		return nil, jsonrpc2.Error{
 814 | 			Code:    jsonrpc2.InternalErrorCode,
 815 | 			Message: fmt.Sprintf("Failed to prepare request: %v", err),
 816 | 		}
 817 | 	}
 818 | 
 819 | 	if req.Path != nil {
 820 | 		r, err := os.Open(*req.Path)
 821 | 		if err != nil {
 822 | 			return nil, jsonrpc2.Error{
 823 | 				Code:    jsonrpc2.InternalErrorCode,
 824 | 				Message: fmt.Sprintf("Failed to open file: %v", err),
 825 | 			}
 826 | 		}
 827 | 		defer r.Close()
 828 | 
 829 | 		if _, err := io.Copy(part, r); err != nil {
 830 | 			return nil, jsonrpc2.Error{
 831 | 				Code:    jsonrpc2.InternalErrorCode,
 832 | 				Message: fmt.Sprintf("Failed to read file content: %v", err),
 833 | 			}
 834 | 		}
 835 | 	} else if req.Base64 {
 836 | 		r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(*req.Content))
 837 | 		if _, err := io.Copy(part, r); err != nil {
 838 | 			return nil, jsonrpc2.Error{
 839 | 				Code:    jsonrpc2.InternalErrorCode,
 840 | 				Message: fmt.Sprintf("Failed to read file content: %v", err),
 841 | 			}
 842 | 		}
 843 | 	} else {
 844 | 		if _, err := part.Write([]byte(*req.Content)); err != nil {
 845 | 			return nil, jsonrpc2.Error{
 846 | 				Code:    jsonrpc2.InternalErrorCode,
 847 | 				Message: fmt.Sprintf("Failed to read file content: %v", err),
 848 | 			}
 849 | 		}
 850 | 	}
 851 | 
 852 | 	if err := mw.Close(); err != nil {
 853 | 		return nil, jsonrpc2.Error{
 854 | 			Code:    jsonrpc2.InternalErrorCode,
 855 | 			Message: fmt.Sprintf("Failed to finalize request: %v", err),
 856 | 		}
 857 | 	}
 858 | 
 859 | 	var res struct {
 860 | 		FileKey string `json:"fileKey"`
 861 | 	}
 862 | 	if err := h.FetchHTTPWithReader(ctx, "POST", "/k/v1/file.json", nil, &body, mw.FormDataContentType(), &res); err != nil {
 863 | 		return nil, err
 864 | 	}
 865 | 
 866 | 	return JSONContent(JsonMap{
 867 | 		"success": true,
 868 | 		"fileKey": res.FileKey,
 869 | 	})
 870 | }
 871 | 
 872 | func (h *KintoneHandlers) ReadRecordComments(ctx context.Context, params json.RawMessage) ([]Content, error) {
 873 | 	var req struct {
 874 | 		AppID    string `json:"appID"`
 875 | 		RecordID string `json:"recordID"`
 876 | 		Order    string `json:"order"`
 877 | 		Offset   int    `json:"offset"`
 878 | 		Limit    *int   `json:"limit"`
 879 | 	}
 880 | 	if err := UnmarshalParams(params, &req); err != nil {
 881 | 		return nil, err
 882 | 	}
 883 | 
 884 | 	if req.AppID == "" || req.RecordID == "" {
 885 | 		return nil, jsonrpc2.Error{
 886 | 			Code:    jsonrpc2.InvalidParamsCode,
 887 | 			Message: "Arguments 'appID' and 'recordID' are required",
 888 | 		}
 889 | 	}
 890 | 
 891 | 	if req.Order == "" {
 892 | 		req.Order = "desc"
 893 | 	} else if req.Order != "asc" && req.Order != "desc" {
 894 | 		return nil, jsonrpc2.Error{
 895 | 			Code:    jsonrpc2.InvalidParamsCode,
 896 | 			Message: "Order must be 'asc' or 'desc'",
 897 | 		}
 898 | 	}
 899 | 
 900 | 	if req.Offset < 0 {
 901 | 		return nil, jsonrpc2.Error{
 902 | 			Code:    jsonrpc2.InvalidParamsCode,
 903 | 			Message: "Offset must be greater than or equal to 0",
 904 | 		}
 905 | 	}
 906 | 
 907 | 	if req.Limit == nil {
 908 | 		limit := 10
 909 | 		req.Limit = &limit
 910 | 	} else if *req.Limit < 0 || *req.Limit > 10 {
 911 | 		return nil, jsonrpc2.Error{
 912 | 			Code:    jsonrpc2.InvalidParamsCode,
 913 | 			Message: "Limit must be between 1 and 10",
 914 | 		}
 915 | 	}
 916 | 
 917 | 	if err := h.checkPermissions(req.AppID); err != nil {
 918 | 		return nil, err
 919 | 	}
 920 | 
 921 | 	httpReq := JsonMap{
 922 | 		"app":    req.AppID,
 923 | 		"record": req.RecordID,
 924 | 		"order":  req.Order,
 925 | 		"offset": req.Offset,
 926 | 		"limit":  *req.Limit,
 927 | 	}
 928 | 	var httpRes struct {
 929 | 		Comments []JsonMap `json:"comments"`
 930 | 		Older    bool      `json:"older"`
 931 | 		Newer    bool      `json:"newer"`
 932 | 	}
 933 | 	if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/record/comments.json", nil, httpReq, &httpRes); err != nil {
 934 | 		return nil, err
 935 | 	}
 936 | 
 937 | 	return JSONContent(JsonMap{
 938 | 		"comments":            httpRes.Comments,
 939 | 		"existsOlderComments": httpRes.Older,
 940 | 		"existsNewerComments": httpRes.Newer,
 941 | 	})
 942 | }
 943 | 
 944 | func (h *KintoneHandlers) CreateRecordComment(ctx context.Context, params json.RawMessage) ([]Content, error) {
 945 | 	var req struct {
 946 | 		AppID    string `json:"appID"`
 947 | 		RecordID string `json:"recordID"`
 948 | 		Comment  struct {
 949 | 			Text     string `json:"text"`
 950 | 			Mentions []struct {
 951 | 				Code string `json:"code"`
 952 | 				Type string `json:"type"`
 953 | 			} `json:"mentions"`
 954 | 		} `json:"comment"`
 955 | 	}
 956 | 	if err := UnmarshalParams(params, &req); err != nil {
 957 | 		return nil, err
 958 | 	}
 959 | 
 960 | 	if req.AppID == "" || req.RecordID == "" || req.Comment.Text == "" {
 961 | 		return nil, jsonrpc2.Error{
 962 | 			Code:    jsonrpc2.InvalidParamsCode,
 963 | 			Message: "Arguments 'appID', 'recordID', and 'comment.text' are required",
 964 | 		}
 965 | 	}
 966 | 
 967 | 	for i, m := range req.Comment.Mentions {
 968 | 		if m.Code == "" {
 969 | 			return nil, jsonrpc2.Error{
 970 | 				Code:    jsonrpc2.InvalidParamsCode,
 971 | 				Message: "Mention code is required",
 972 | 			}
 973 | 		}
 974 | 		if m.Type == "" {
 975 | 			req.Comment.Mentions[i].Type = "USER"
 976 | 		} else if m.Type != "USER" && m.Type != "GROUP" && m.Type != "ORGANIZATION" {
 977 | 			return nil, jsonrpc2.Error{
 978 | 				Code:    jsonrpc2.InvalidParamsCode,
 979 | 				Message: "Mention type must be 'USER', 'GROUP', or 'ORGANIZATION'",
 980 | 			}
 981 | 		}
 982 | 	}
 983 | 
 984 | 	if err := h.checkPermissions(req.AppID); err != nil {
 985 | 		return nil, err
 986 | 	}
 987 | 
 988 | 	httpReq := JsonMap{
 989 | 		"app":     req.AppID,
 990 | 		"record":  req.RecordID,
 991 | 		"comment": req.Comment,
 992 | 	}
 993 | 	if err := h.FetchHTTPWithJSON(ctx, "POST", "/k/v1/record/comment.json", nil, httpReq, nil); err != nil {
 994 | 		return nil, err
 995 | 	}
 996 | 
 997 | 	return JSONContent(JsonMap{
 998 | 		"success": true,
 999 | 	})
1000 | }
1001 | 
1002 | func (h *KintoneHandlers) UpdateProcessManagementAssignee(ctx context.Context, params json.RawMessage) ([]Content, error) {
1003 | 	var req struct {
1004 | 		AppID     string   `json:"appID"`
1005 | 		RecordID  string   `json:"recordID"`
1006 | 		Assignees []string `json:"assignees"`
1007 | 	}
1008 | 	if err := UnmarshalParams(params, &req); err != nil {
1009 | 		return nil, err
1010 | 	}
1011 | 	if req.AppID == "" || req.RecordID == "" {
1012 | 		return nil, jsonrpc2.Error{
1013 | 			Code:    jsonrpc2.InvalidParamsCode,
1014 | 			Message: "Arguments 'appID' and 'recordID' are required",
1015 | 		}
1016 | 	}
1017 | 
1018 | 	httpReq := JsonMap{
1019 | 		"app":       req.AppID,
1020 | 		"id":        req.RecordID,
1021 | 		"assignees": req.Assignees,
1022 | 	}
1023 | 	if err := h.FetchHTTPWithJSON(ctx, "PUT", "/k/v1/record/assignees.json", nil, httpReq, nil); err != nil {
1024 | 		return nil, err
1025 | 	}
1026 | 
1027 | 	return JSONContent(JsonMap{
1028 | 		"success": true,
1029 | 	})
1030 | }
1031 | 
1032 | func (h *KintoneHandlers) ExecuteProcessManagementAction(ctx context.Context, params json.RawMessage) ([]Content, error) {
1033 | 	var req struct {
1034 | 		AppID    string  `json:"appID"`
1035 | 		RecordID string  `json:"recordID"`
1036 | 		Action   string  `json:"action"`
1037 | 		Assignee *string `json:"assignee"`
1038 | 	}
1039 | 	if err := UnmarshalParams(params, &req); err != nil {
1040 | 		return nil, err
1041 | 	}
1042 | 	if req.AppID == "" || req.RecordID == "" || req.Action == "" {
1043 | 		return nil, jsonrpc2.Error{
1044 | 			Code:    jsonrpc2.InvalidParamsCode,
1045 | 			Message: "Arguments 'appID', 'recordID', and 'action' are required",
1046 | 		}
1047 | 	}
1048 | 
1049 | 	httpReq := JsonMap{
1050 | 		"app":    req.AppID,
1051 | 		"id":     req.RecordID,
1052 | 		"action": req.Action,
1053 | 	}
1054 | 	if req.Assignee != nil {
1055 | 		httpReq["assignee"] = *req.Assignee
1056 | 	}
1057 | 	if err := h.FetchHTTPWithJSON(ctx, "PUT", "/k/v1/record/status.json", nil, httpReq, nil); err != nil {
1058 | 		return nil, err
1059 | 	}
1060 | 
1061 | 	return JSONContent(JsonMap{
1062 | 		"success": true,
1063 | 	})
1064 | }
1065 | 
1066 | func Getenv(key, defaultValue string) string {
1067 | 	if v := os.Getenv(key); v != "" {
1068 | 		return v
1069 | 	}
1070 | 	return defaultValue
1071 | }
1072 | 
1073 | func GetenvList(key string) []string {
1074 | 	if v := os.Getenv(key); v != "" {
1075 | 		raw := strings.Split(v, ",")
1076 | 		ss := make([]string, 0, len(raw))
1077 | 		for _, s := range raw {
1078 | 			if s != "" {
1079 | 				ss = append(ss, strings.TrimSpace(s))
1080 | 			}
1081 | 		}
1082 | 		return ss
1083 | 	}
1084 | 	return nil
1085 | }
1086 | 
1087 | type MergedReadWriter struct {
1088 | 	r io.Reader
1089 | 	w io.Writer
1090 | }
1091 | 
1092 | func (rw *MergedReadWriter) Read(p []byte) (int, error) {
1093 | 	return rw.r.Read(p)
1094 | }
1095 | 
1096 | func (rw *MergedReadWriter) Write(p []byte) (int, error) {
1097 | 	return rw.w.Write(p)
1098 | }
1099 | 
1100 | func main() {
1101 | 	handlers, err := NewKintoneHandlersFromEnv()
1102 | 	if err != nil {
1103 | 		fmt.Fprintf(os.Stderr, "%s\n", err)
1104 | 		os.Exit(1)
1105 | 	}
1106 | 
1107 | 	server := jsonrpc2.NewServer()
1108 | 	server.On("initialize", jsonrpc2.Call(handlers.InitializeHandler))
1109 | 	server.On("notifications/initialized", jsonrpc2.Notify(func(ctx context.Context, params any) error {
1110 | 		return nil
1111 | 	}))
1112 | 	server.On("ping", jsonrpc2.Call(func(ctx context.Context, params any) (struct{}, error) {
1113 | 		return struct{}{}, nil
1114 | 	}))
1115 | 	server.On("tools/list", jsonrpc2.Call(handlers.ToolsList))
1116 | 	server.On("tools/call", jsonrpc2.Call(handlers.ToolsCall))
1117 | 
1118 | 	fmt.Fprintf(os.Stderr, "kintone server is running on stdio!\n")
1119 | 
1120 | 	server.ServeForOne(&MergedReadWriter{r: os.Stdin, w: os.Stdout})
1121 | }
1122 | 
```