mcp-server-kintone
by macrat
package main
import (
"bytes"
"context"
_ "embed"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"mime/multipart"
"net/http"
"net/url"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"text/template"
"github.com/macrat/go-jsonrpc2"
)
var (
Version = "UNKNOWN"
Commit = "HEAD"
)
type JsonMap map[string]any
type ServerInfo struct {
Name string `json:"name"`
Version string `json:"version"`
}
type InitializeResult struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities JsonMap `json:"capabilities"`
ServerInfo ServerInfo `json:"serverInfo"`
Instructions string `json:"instructions"`
}
type Content struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
Data string `json:"data,omitempty"`
MimeType string `json:"mimeType,omitempty"`
}
func JSONContent(v any) ([]Content, error) {
bs, err := json.MarshalIndent(v, "", " ")
if err != nil {
return nil, err
}
return []Content{{Type: "text", Text: string(bs)}}, nil
}
type ToolInfo struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
InputSchema JsonMap `json:"inputSchema"`
}
type ToolsListResult struct {
Tools []ToolInfo `json:"tools"`
}
type ToolsCallRequest struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}
type ToolsCallResult struct {
Content []Content `json:"content"`
IsError bool `json:"isError"`
}
func UnmarshalParams[T any](data []byte, target *T) error {
err := json.Unmarshal(data, target)
if err != nil {
return jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: fmt.Sprintf("Failed to parse parameters: %v", err),
}
}
return nil
}
type KintoneAppDetail struct {
AppID string `json:"appID"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Properties JsonMap `json:"properties,omitempty"`
CreatedAt string `json:"createdAt"`
ModifiedAt string `json:"modifiedAt"`
}
type KintoneHandlers struct {
URL *url.URL
Auth string
Token string
Allow []string
Deny []string
}
func NewKintoneHandlersFromEnv() (*KintoneHandlers, error) {
var handlers KintoneHandlers
errs := []error{errors.New("Error:")}
username := Getenv("KINTONE_USERNAME", "")
password := Getenv("KINTONE_PASSWORD", "")
tokens := Getenv("KINTONE_API_TOKEN", "")
if (username == "" || password == "") && tokens == "" {
errs = append(errs, errors.New("- Either KINTONE_USERNAME/KINTONE_PASSWORD or KINTONE_API_TOKEN must be provided"))
}
if username != "" && password != "" {
handlers.Auth = base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", username, password))
}
handlers.Token = tokens
baseURL := Getenv("KINTONE_BASE_URL", "")
if baseURL == "" {
errs = append(errs, errors.New("- KINTONE_BASE_URL must be provided"))
} else if u, err := url.Parse(baseURL); err != nil {
errs = append(errs, fmt.Errorf("- Failed to parse KINTONE_BASE_URL: %s", err))
} else {
handlers.URL = u
}
handlers.Allow = GetenvList("KINTONE_ALLOW_APPS")
handlers.Deny = GetenvList("KINTONE_DENY_APPS")
if len(errs) > 1 {
return nil, errors.Join(errs...)
}
return &handlers, nil
}
type Query map[string]string
func (q Query) Encode() string {
values := make(url.Values)
for k, v := range q {
values.Set(k, v)
}
return values.Encode()
}
func (h *KintoneHandlers) SendHTTP(ctx context.Context, method, path string, query Query, body io.Reader, contentType string) (*http.Response, error) {
endpoint := h.URL.JoinPath(path)
endpoint.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), body)
if err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to create HTTP request: %v", err),
}
}
if h.Auth != "" {
req.Header.Set("X-Cybozu-Authorization", h.Auth)
}
if h.Token != "" {
req.Header.Set("X-Cybozu-API-Token", h.Token)
}
if body != nil {
req.Header.Set("Content-Type", contentType)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to send HTTP request to kintone server: %v", err),
}
}
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("kintone server returned an error: %s\n%s", res.Status, msg),
}
}
return res, nil
}
func (h *KintoneHandlers) FetchHTTPWithReader(ctx context.Context, method, path string, query Query, body io.Reader, contentType string, result any) error {
res, err := h.SendHTTP(ctx, method, path, query, body, contentType)
if err != nil {
return err
}
defer res.Body.Close()
if result != nil {
if err := json.NewDecoder(res.Body).Decode(result); err != nil {
return jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to parse kintone server's response: %v", err),
}
}
}
return nil
}
func (h *KintoneHandlers) FetchHTTPWithJSON(ctx context.Context, method, path string, query Query, body, result any) error {
var reqBody io.Reader
if body != nil {
bs, err := json.Marshal(body)
if err != nil {
return jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to prepare request body for kintone server: %v", err),
}
}
reqBody = bytes.NewReader(bs)
}
return h.FetchHTTPWithReader(ctx, method, path, query, reqBody, "application/json", result)
}
func (h *KintoneHandlers) InitializeHandler(ctx context.Context, params any) (InitializeResult, error) {
return InitializeResult{
ProtocolVersion: "2024-11-05",
Capabilities: JsonMap{
"tools": JsonMap{},
},
ServerInfo: ServerInfo{
Name: "Kintone Server",
Version: fmt.Sprintf("%s (%s)", Version, Commit),
},
Instructions: fmt.Sprintf("kintone is a database service to store and manage enterprise data. You can use this server to interact with kintone."),
}, nil
}
//go:embed tools_list.json
var toolsListTmplStr string
var toolsList ToolsListResult
func init() {
tmpl, err := template.New("tools_list").Parse(toolsListTmplStr)
if err != nil {
panic(fmt.Sprintf("Failed to parse tools list template: %v", err))
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, nil); err != nil {
panic(fmt.Sprintf("Failed to render tools list template: %v", err))
}
if err := json.Unmarshal(buf.Bytes(), &toolsList); err != nil {
panic(fmt.Sprintf("Failed to parse tools list JSON: %v", err))
}
}
func (h *KintoneHandlers) ToolsList(ctx context.Context, params any) (ToolsListResult, error) {
return toolsList, nil
}
func (h *KintoneHandlers) ToolsCall(ctx context.Context, params ToolsCallRequest) (ToolsCallResult, error) {
var content []Content
var err error
switch params.Name {
case "listApps":
content, err = h.ListApps(ctx, params.Arguments)
case "readAppInfo":
content, err = h.ReadAppInfo(ctx, params.Arguments)
case "createRecord":
content, err = h.CreateRecord(ctx, params.Arguments)
case "readRecords":
content, err = h.ReadRecords(ctx, params.Arguments)
case "updateRecord":
content, err = h.UpdateRecord(ctx, params.Arguments)
case "deleteRecord":
content, err = h.DeleteRecord(ctx, params.Arguments)
case "downloadAttachmentFile":
content, err = h.DownloadAttachmentFile(ctx, params.Arguments)
case "uploadAttachmentFile":
content, err = h.UploadAttachmentFile(ctx, params.Arguments)
case "readRecordComments":
content, err = h.ReadRecordComments(ctx, params.Arguments)
case "createRecordComment":
content, err = h.CreateRecordComment(ctx, params.Arguments)
default:
return ToolsCallResult{}, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: fmt.Sprintf("Unknown tool name: %s", params.Name),
}
}
if err != nil {
return ToolsCallResult{}, err
}
return ToolsCallResult{
Content: content,
}, nil
}
func (h *KintoneHandlers) checkPermissions(id string) error {
if slices.Contains(h.Deny, id) {
return jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
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),
}
}
if len(h.Allow) > 0 && !slices.Contains(h.Allow, id) {
return jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
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),
}
}
return nil
}
func (h *KintoneHandlers) ListApps(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
Offset int `json:"offset"`
Limit *int `json:"limit"`
Name *string `json:"name"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.Offset < 0 {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Offset must be greater than or equal to 0",
}
}
if req.Limit == nil {
limit := 100
req.Limit = &limit
} else if *req.Limit < 1 || *req.Limit > 100 {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Limit must be between 1 and 100",
}
}
type Res struct {
Apps []KintoneAppDetail `json:"apps"`
}
var httpRes Res
err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/apps.json", nil, req, &httpRes)
if err != nil {
return nil, err
}
apps := make([]KintoneAppDetail, 0, len(httpRes.Apps))
for _, app := range httpRes.Apps {
if err := h.checkPermissions(app.AppID); err == nil {
apps = append(apps, app)
}
}
hasNext := false
var httpRes2 Res
err = h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/apps.json", nil, JsonMap{"offset": req.Offset + len(httpRes.Apps), "limit": 1}, &httpRes2)
if err == nil {
hasNext = len(httpRes2.Apps) > 0
}
return JSONContent(JsonMap{
"apps": apps,
"hasNext": hasNext,
})
}
func (h *KintoneHandlers) ReadAppInfo(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
AppID string `json:"appID"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.AppID == "" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Argument 'appID' is required",
}
}
if err := h.checkPermissions(req.AppID); err != nil {
return nil, err
}
var app KintoneAppDetail
if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/app.json", Query{"id": req.AppID}, nil, &app); err != nil {
return nil, err
}
var fields struct {
Properties JsonMap `json:"properties"`
}
if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/app/form/fields.json", Query{"app": req.AppID}, nil, &fields); err != nil {
return nil, err
}
app.Properties = fields.Properties
return JSONContent(app)
}
func (h *KintoneHandlers) CreateRecord(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
AppID string `json:"appID"`
Record JsonMap `json:"record"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.AppID == "" || req.Record == nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Arguments 'appID' and 'record' are required",
}
}
if err := h.checkPermissions(req.AppID); err != nil {
return nil, err
}
httpReq := JsonMap{
"app": req.AppID,
"record": req.Record,
}
var record struct {
ID string `json:"id"`
}
if err := h.FetchHTTPWithJSON(ctx, "POST", "/k/v1/record.json", nil, httpReq, &record); err != nil {
return nil, err
}
return JSONContent(JsonMap{
"success": true,
"recordID": record.ID,
})
}
func (h *KintoneHandlers) ReadRecords(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
AppID string `json:"appID"`
Query string `json:"query"`
Limit *int `json:"limit"`
Fields []string `json:"fields"`
Offset int `json:"offset"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.AppID == "" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Argument 'appID' is required",
}
}
if req.Limit == nil {
limit := 10
req.Limit = &limit
} else if *req.Limit < 1 || *req.Limit > 500 {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Limit must be between 1 and 500",
}
}
if req.Offset < 0 || req.Offset > 10000 {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Offset must be between 0 and 10000",
}
}
if err := h.checkPermissions(req.AppID); err != nil {
return nil, err
}
httpReq := JsonMap{
"app": req.AppID,
"query": req.Query,
"limit": *req.Limit,
"offset": req.Offset,
"fields": req.Fields,
"totalCount": true,
}
var records JsonMap
if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/records.json", nil, httpReq, &records); err != nil {
return nil, err
}
return JSONContent(records)
}
func (h *KintoneHandlers) UpdateRecord(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
AppID string `json:"appID"`
RecordID string `json:"recordID"`
Record any `json:"record"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.AppID == "" || req.RecordID == "" || req.Record == nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Arguments 'appID', 'recordID', and 'record' are required",
}
}
if err := h.checkPermissions(req.AppID); err != nil {
return nil, err
}
httpReq := JsonMap{
"app": req.AppID,
"id": req.RecordID,
"record": req.Record,
}
var result struct {
Revision string `json:"revision"`
}
if err := h.FetchHTTPWithJSON(ctx, "PUT", "/k/v1/record.json", nil, httpReq, &result); err != nil {
return nil, err
}
return JSONContent(JsonMap{
"success": true,
"revision": result.Revision,
})
}
func (h *KintoneHandlers) readSingleRecord(ctx context.Context, appID, recordID string) (JsonMap, error) {
var result struct {
Record JsonMap `json:"record"`
}
err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/record.json", Query{"app": appID, "id": recordID}, nil, &result)
return result.Record, err
}
func (h *KintoneHandlers) DeleteRecord(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
AppID string `json:"appID"`
RecordID string `json:"recordID"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.AppID == "" || req.RecordID == "" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Arguments 'appID' and 'recordID' are required",
}
}
if err := h.checkPermissions(req.AppID); err != nil {
return nil, err
}
deletedRecord, err := h.readSingleRecord(ctx, req.AppID, req.RecordID)
if err != nil {
return nil, err
}
if err := h.FetchHTTPWithJSON(ctx, "DELETE", "/k/v1/records.json", Query{"app": req.AppID, "ids[0]": req.RecordID}, nil, nil); err != nil {
return nil, err
}
result := JsonMap{
"success": true,
}
if deletedRecord != nil {
result["deletedRecord"] = deletedRecord
}
return JSONContent(result)
}
func getDownloadDirectory() string {
dir, err := os.UserHomeDir()
if err != nil {
return os.TempDir()
}
for _, d := range []string{"Downloads", "downloads", "Download", "download"} {
d = filepath.Join(dir, d)
if _, err := os.Stat(d); err == nil {
return d
}
}
dir = filepath.Join(dir, "Downloads")
err = os.MkdirAll(dir, 0755)
if err != nil {
return os.TempDir()
}
return dir
}
func getDownloadFilePath(fileName string) string {
dir := getDownloadDirectory()
p := filepath.Join(dir, fileName)
if _, err := os.Stat(p); err != nil {
return p
}
ext := filepath.Ext(fileName)
base := strings.TrimSuffix(fileName, ext)
num := 1
if strings.HasSuffix(base, ")") {
if i := strings.LastIndex(base, " ("); i > 0 {
if n, err := strconv.Atoi(base[i+2:]); err == nil {
base = base[:i]
num = n
}
}
}
for {
p = filepath.Join(dir, fmt.Sprintf("%s (%d)%s", base, num, ext))
if _, err := os.Stat(p); err != nil {
return p
}
num++
}
}
func (h *KintoneHandlers) DownloadAttachmentFile(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
FileKey string `json:"fileKey"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.FileKey == "" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Argument 'fileKey' is required",
}
}
httpRes, err := h.SendHTTP(ctx, "GET", "/k/v1/file.json", Query{"fileKey": req.FileKey}, nil, "")
if err != nil {
return nil, err
}
defer httpRes.Body.Close()
contentType := httpRes.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
var fileName string
_, ps, err := mime.ParseMediaType(httpRes.Header.Get("Content-Disposition"))
if err == nil {
fileName = ps["filename"]
}
fileName, err = new(mime.WordDecoder).DecodeHeader(fileName)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to decode filename: %v\n", err)
fileName = ""
}
if fileName == "" {
fileName = req.FileKey
ext, err := mime.ExtensionsByType(contentType)
if err == nil && len(ext) > 0 {
fileName += ext[0]
}
}
outPath := getDownloadFilePath(fileName)
outFile, err := os.Create(outPath)
if err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to create file for attachment: %v", err),
Data: JsonMap{"filePath": outPath},
}
}
defer outFile.Close()
var w io.Writer = outFile
var buf *bytes.Buffer
if strings.HasPrefix(contentType, "text/") || strings.HasPrefix(contentType, "image/") {
buf = new(bytes.Buffer)
w = io.MultiWriter(outFile, buf)
}
size, err := io.Copy(w, httpRes.Body)
if err != nil {
outFile.Close()
os.Remove(outPath)
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to save attachment file: %s: %v", outPath, err),
}
}
res, err := JSONContent(JsonMap{
"success": true,
"filePath": outPath,
"size": size,
})
if err != nil {
return nil, err
}
if strings.HasPrefix(contentType, "text/") {
res = append(res, Content{Type: "text", Text: buf.String()})
} else if strings.HasPrefix(contentType, "image/") {
b64 := base64.StdEncoding.EncodeToString(buf.Bytes())
res = append(res, Content{
Type: "image",
Data: b64,
MimeType: contentType,
})
}
return res, nil
}
func (h *KintoneHandlers) UploadAttachmentFile(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
Path *string `json:"path"`
Name string `json:"name"`
Content *string `json:"content"`
Base64 bool `json:"base64"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.Path == nil && req.Content == nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Arguments 'path' or 'content' is required",
}
}
if req.Path != nil && req.Content != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Arguments 'path' and 'content' are mutually exclusive",
}
}
var filename string
if req.Path != nil {
filename = filepath.Base(*req.Path)
} else {
filename = req.Name
if filename == "" {
filename = "file"
ext, err := mime.ExtensionsByType(mime.TypeByExtension(filepath.Ext(req.Name)))
if err == nil && len(ext) > 0 {
filename += ext[0]
}
}
}
var body bytes.Buffer
mw := multipart.NewWriter(&body)
part, err := mw.CreateFormFile("file", filename)
if err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to prepare request: %v", err),
}
}
if req.Path != nil {
r, err := os.Open(*req.Path)
if err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to open file: %v", err),
}
}
defer r.Close()
if _, err := io.Copy(part, r); err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to read file content: %v", err),
}
}
} else if req.Base64 {
r := base64.NewDecoder(base64.StdEncoding, strings.NewReader(*req.Content))
if _, err := io.Copy(part, r); err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to read file content: %v", err),
}
}
} else {
if _, err := part.Write([]byte(*req.Content)); err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to read file content: %v", err),
}
}
}
if err := mw.Close(); err != nil {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InternalErrorCode,
Message: fmt.Sprintf("Failed to finalize request: %v", err),
}
}
var res struct {
FileKey string `json:"fileKey"`
}
if err := h.FetchHTTPWithReader(ctx, "POST", "/k/v1/file.json", nil, &body, mw.FormDataContentType(), &res); err != nil {
return nil, err
}
return JSONContent(JsonMap{
"success": true,
"fileKey": res.FileKey,
})
}
func (h *KintoneHandlers) ReadRecordComments(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
AppID string `json:"appID"`
RecordID string `json:"recordID"`
Order string `json:"order"`
Offset int `json:"offset"`
Limit *int `json:"limit"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.AppID == "" || req.RecordID == "" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Arguments 'appID' and 'recordID' are required",
}
}
if req.Order == "" {
req.Order = "desc"
} else if req.Order != "asc" && req.Order != "desc" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Order must be 'asc' or 'desc'",
}
}
if req.Offset < 0 {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Offset must be greater than or equal to 0",
}
}
if req.Limit == nil {
limit := 10
req.Limit = &limit
} else if *req.Limit < 0 || *req.Limit > 10 {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Limit must be between 1 and 10",
}
}
if err := h.checkPermissions(req.AppID); err != nil {
return nil, err
}
httpReq := JsonMap{
"app": req.AppID,
"record": req.RecordID,
"order": req.Order,
"offset": req.Offset,
"limit": *req.Limit,
}
var httpRes struct {
Comments []JsonMap `json:"comments"`
Older bool `json:"older"`
Newer bool `json:"newer"`
}
if err := h.FetchHTTPWithJSON(ctx, "GET", "/k/v1/record/comments.json", nil, httpReq, &httpRes); err != nil {
return nil, err
}
return JSONContent(JsonMap{
"comments": httpRes.Comments,
"existsOlderComments": httpRes.Older,
"existsNewerComments": httpRes.Newer,
})
}
func (h *KintoneHandlers) CreateRecordComment(ctx context.Context, params json.RawMessage) ([]Content, error) {
var req struct {
AppID string `json:"appID"`
RecordID string `json:"recordID"`
Comment struct {
Text string `json:"text"`
Mentions []struct {
Code string `json:"code"`
Type string `json:"type"`
} `json:"mentions"`
} `json:"comment"`
}
if err := UnmarshalParams(params, &req); err != nil {
return nil, err
}
if req.AppID == "" || req.RecordID == "" || req.Comment.Text == "" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Arguments 'appID', 'recordID', and 'comment.text' are required",
}
}
for i, m := range req.Comment.Mentions {
if m.Code == "" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Mention code is required",
}
}
if m.Type == "" {
req.Comment.Mentions[i].Type = "USER"
} else if m.Type != "USER" && m.Type != "GROUP" && m.Type != "ORGANIZATION" {
return nil, jsonrpc2.Error{
Code: jsonrpc2.InvalidParamsCode,
Message: "Mention type must be 'USER', 'GROUP', or 'ORGANIZATION'",
}
}
}
if err := h.checkPermissions(req.AppID); err != nil {
return nil, err
}
httpReq := JsonMap{
"app": req.AppID,
"record": req.RecordID,
"comment": req.Comment,
}
if err := h.FetchHTTPWithJSON(ctx, "POST", "/k/v1/record/comment.json", nil, httpReq, nil); err != nil {
return nil, err
}
return JSONContent(JsonMap{
"success": true,
})
}
func Getenv(key, defaultValue string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultValue
}
func GetenvList(key string) []string {
if v := os.Getenv(key); v != "" {
raw := strings.Split(v, ",")
ss := make([]string, 0, len(raw))
for _, s := range raw {
if s != "" {
ss = append(ss, strings.TrimSpace(s))
}
}
return ss
}
return nil
}
type MergedReadWriter struct {
r io.Reader
w io.Writer
}
func (rw *MergedReadWriter) Read(p []byte) (int, error) {
return rw.r.Read(p)
}
func (rw *MergedReadWriter) Write(p []byte) (int, error) {
return rw.w.Write(p)
}
func main() {
handlers, err := NewKintoneHandlersFromEnv()
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(1)
}
server := jsonrpc2.NewServer()
server.On("initialize", jsonrpc2.Call(handlers.InitializeHandler))
server.On("notifications/initialized", jsonrpc2.Notify(func(ctx context.Context, params any) error {
return nil
}))
server.On("ping", jsonrpc2.Call(func(ctx context.Context, params any) (struct{}, error) {
return struct{}{}, nil
}))
server.On("tools/list", jsonrpc2.Call(handlers.ToolsList))
server.On("tools/call", jsonrpc2.Call(handlers.ToolsCall))
fmt.Fprintf(os.Stderr, "kintone server is running on stdio!\n")
server.ServeForOne(&MergedReadWriter{r: os.Stdin, w: os.Stdout})
}