This is page 1 of 3. Use http://codebase.md/r3-yamauchi/kintone-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── .mcpbignore ├── .npmrc ├── AGENTS.md ├── CLAUDE.md ├── docs │ ├── images │ │ ├── mcpb-install0.png │ │ ├── mcpb-install1.png │ │ ├── mcpb-install2.png │ │ ├── mcpb-install3.png │ │ ├── mcpb-install4.png │ │ ├── mcpb-install5.png │ │ ├── mcpb-install6.png │ │ ├── mcpb-install7.png │ │ ├── tool.png │ │ └── tools.png │ ├── mcp-server-architecture.md │ ├── mcp-specification │ │ ├── 2024-11-05.txt │ │ └── 2025-03-26.txt │ └── tool-annotations.md ├── LICENSE ├── manifest.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── README.md ├── renovate.json ├── scripts │ └── ensure-pnpm.js ├── server.js ├── src │ ├── constants.js │ ├── index.js │ ├── models │ │ ├── KintoneCredentials.js │ │ └── KintoneRecord.js │ ├── repositories │ │ ├── base │ │ │ ├── BaseKintoneRepository.js │ │ │ └── http │ │ │ ├── createKintoneClient.js │ │ │ ├── KintoneApiError.js │ │ │ └── KintoneHttpClient.js │ │ ├── KintoneAppRepository.js │ │ ├── KintoneFileRepository.js │ │ ├── KintoneFormRepository.js │ │ ├── KintonePreviewRepository.js │ │ ├── KintoneRecordRepository.js │ │ ├── KintoneRepository.js │ │ ├── KintoneSpaceRepository.js │ │ ├── KintoneUserRepository.js │ │ └── validators │ │ ├── FieldValidator.js │ │ ├── LayoutValidator.js │ │ └── OptionValidator.js │ ├── server │ │ ├── handlers │ │ │ ├── ErrorHandlers.js │ │ │ ├── ToolRequestHandler.js │ │ │ └── ToolRouter.js │ │ ├── MCPServer.js │ │ └── tools │ │ ├── AppTools.js │ │ ├── BaseToolHandler.js │ │ ├── definitions │ │ │ ├── AppToolDefinitions.js │ │ │ ├── DocumentationToolDefinitions.js │ │ │ ├── FieldToolDefinitions.js │ │ │ ├── FileToolDefinitions.js │ │ │ ├── index.js │ │ │ ├── LayoutToolDefinitions.js │ │ │ ├── RecordToolDefinitions.js │ │ │ ├── SpaceToolDefinitions.js │ │ │ ├── SystemToolDefinitions.js │ │ │ └── UserToolDefinitions.js │ │ ├── documentation │ │ │ ├── calcField.js │ │ │ ├── choiceFields.js │ │ │ ├── dateField.js │ │ │ ├── dateTimeField.js │ │ │ ├── fileField.js │ │ │ ├── index.js │ │ │ ├── layoutFields.js │ │ │ ├── linkField.js │ │ │ ├── lookup.js │ │ │ ├── numberField.js │ │ │ ├── otherFields.js │ │ │ ├── queryLanguage.js │ │ │ ├── referenceTable.js │ │ │ ├── richText.js │ │ │ ├── subtable.js │ │ │ ├── systemFields.js │ │ │ ├── textFields.js │ │ │ ├── timeField.js │ │ │ └── userSelect.js │ │ ├── DocumentationTools.js │ │ ├── FieldTools.js │ │ ├── FileTools.js │ │ ├── LayoutTools.js │ │ ├── RecordTools.js │ │ ├── SpaceTools.js │ │ ├── SystemTools.js │ │ └── UserTools.js │ └── utils │ ├── DataTransformers.js │ ├── FieldValidationUtils.js │ ├── index.js │ ├── LayoutUtils.js │ ├── LoggingUtils.js │ ├── ResponseBuilder.js │ └── ValidationUtils.js └── unofficial-kintone-mcp-server-8.0.0.mcpb ``` # Files -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- ``` 1 | minimumReleaseAge=1440 2 | ``` -------------------------------------------------------------------------------- /.mcpbignore: -------------------------------------------------------------------------------- ``` 1 | .env 2 | .github/ 3 | docs/ 4 | AGENTS.md 5 | CLAUDE.md 6 | llms-install.md 7 | renovate.json ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Package manager configuration 2 | .pnpm-store/ 3 | 4 | # Standalone build artifacts 5 | standalone/ 6 | standalone-package/ 7 | *.tgz 8 | 9 | # Test directories 10 | test-package/ 11 | coverage/ 12 | 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | .pnpm-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional stylelint cache 70 | .stylelintcache 71 | 72 | # Microbundle cache 73 | .rpt2_cache/ 74 | .rts2_cache_cjs/ 75 | .rts2_cache_es/ 76 | .rts2_cache_umd/ 77 | 78 | # Optional REPL history 79 | .node_repl_history 80 | 81 | # Output of 'npm pack' 82 | *.tgz 83 | 84 | # Yarn Integrity file 85 | .yarn-integrity 86 | 87 | # dotenv environment variable files 88 | .env 89 | .env.development.local 90 | .env.test.local 91 | .env.production.local 92 | .env.local 93 | 94 | # parcel-bundler cache (https://parceljs.org/) 95 | .cache 96 | .parcel-cache 97 | 98 | # Next.js build output 99 | .next 100 | out 101 | 102 | # Nuxt.js build / generate output 103 | .nuxt 104 | dist 105 | 106 | # Gatsby files 107 | .cache/ 108 | # Comment in the public line in if your project uses Gatsby and not Next.js 109 | # https://nextjs.org/blog/next-9-1#public-directory-support 110 | # public 111 | 112 | # vuepress build output 113 | .vuepress/dist 114 | 115 | # vuepress v2.x temp and cache directory 116 | .temp 117 | .cache 118 | 119 | # Docusaurus cache and generated files 120 | .docusaurus 121 | 122 | # Serverless directories 123 | .serverless/ 124 | 125 | # FuseBox cache 126 | .fusebox/ 127 | 128 | # DynamoDB Local files 129 | .dynamodb/ 130 | 131 | # TernJS port file 132 | .tern-port 133 | 134 | # Stores VSCode versions used for testing VSCode extensions 135 | .vscode-test 136 | 137 | # yarn v2 138 | .yarn/cache 139 | .yarn/unplugged 140 | .yarn/build-state.yml 141 | .yarn/install-state.gz 142 | .pnp.* 143 | 144 | # Project specific files 145 | .clinerules 146 | .clinerules/ 147 | 148 | # Editor directories and files 149 | .idea/ 150 | .vscode/ 151 | *.suo 152 | *.ntvs* 153 | *.njsproj 154 | *.sln 155 | *.sw? 156 | 157 | # OS generated files 158 | .DS_Store 159 | .DS_Store? 160 | ._* 161 | .Spotlight-V100 162 | .Trashes 163 | ehthumbs.db 164 | Thumbs.db 165 | 166 | # Local config files 167 | *.local.js 168 | config.local.js 169 | ``` -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | import './src/index.js'; 3 | ``` -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | ``` -------------------------------------------------------------------------------- /src/models/KintoneRecord.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/models/KintoneRecord.js 2 | export class KintoneRecord { 3 | constructor(appId, recordId, fields) { 4 | this.appId = appId; 5 | this.recordId = recordId; 6 | this.fields = fields; 7 | } 8 | } 9 | ``` -------------------------------------------------------------------------------- /src/models/KintoneCredentials.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/models/KintoneCredentials.js 2 | export class KintoneCredentials { 3 | constructor(domain, username, password) { 4 | this.domain = domain; 5 | this.username = username; 6 | this.password = password; 7 | this.auth = Buffer.from(`${username}:${password}`).toString('base64'); 8 | } 9 | } 10 | ``` -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/utils/index.js 2 | export { ValidationUtils } from './ValidationUtils.js'; 3 | export { LoggingUtils } from './LoggingUtils.js'; 4 | export { ResponseBuilder } from './ResponseBuilder.js'; 5 | export { FieldValidationUtils } from './FieldValidationUtils.js'; 6 | export { DataTransformers } from './DataTransformers.js'; 7 | export { LayoutUtils } from './LayoutUtils.js'; ``` -------------------------------------------------------------------------------- /src/repositories/base/http/KintoneApiError.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/repositories/base/http/KintoneApiError.js 2 | export class KintoneApiError extends Error { 3 | constructor(message, { status = null, code = null, errors = null, responseBody = null } = {}) { 4 | super(message); 5 | this.name = 'KintoneApiError'; 6 | this.status = status; 7 | this.code = code; 8 | this.errors = errors; 9 | this.responseBody = responseBody; 10 | } 11 | } 12 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "unofficial-kintone-mcp-server", 3 | "version": "8.0.0", 4 | "packageManager": "[email protected]", 5 | "main": "server.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node server.js", 9 | "test": "node --test" 10 | }, 11 | "keywords": [ 12 | "modelcontextprotocol", 13 | "mcp", 14 | "kintone" 15 | ], 16 | "author": { 17 | "name": "r3-yamauchi", 18 | "url": "https://www.r3it.com/blog/author/yamauchi" 19 | }, 20 | "license": "AGPL-3.0", 21 | "description": "MCP server for kintone (Unofficial)", 22 | "engines": { 23 | "node": ">=20" 24 | }, 25 | "dependencies": { 26 | "@modelcontextprotocol/sdk": "1.18.2" 27 | } 28 | } 29 | ``` -------------------------------------------------------------------------------- /src/utils/DataTransformers.js: -------------------------------------------------------------------------------- ```javascript 1 | import { LoggingUtils } from './LoggingUtils.js'; 2 | 3 | export function convertDropdownFieldType(obj) { 4 | if (!obj || typeof obj !== 'object') return; 5 | 6 | for (const key in obj) { 7 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 8 | const value = obj[key]; 9 | 10 | if (key === 'type' && value === 'DROPDOWN') { 11 | obj[key] = 'DROP_DOWN'; 12 | LoggingUtils.debug('field', 'dropdown_type_normalized', { property: key }); 13 | } 14 | else if (key === 'field_type' && value === 'DROPDOWN') { 15 | obj[key] = 'DROP_DOWN'; 16 | LoggingUtils.debug('field', 'dropdown_type_normalized', { property: key }); 17 | } 18 | else if (value && typeof value === 'object') { 19 | convertDropdownFieldType(value); 20 | } 21 | } 22 | } 23 | } 24 | ``` -------------------------------------------------------------------------------- /src/server/tools/definitions/index.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/server/tools/definitions/index.js 2 | 3 | import { recordToolDefinitions } from './RecordToolDefinitions.js'; 4 | import { appToolDefinitions } from './AppToolDefinitions.js'; 5 | import { spaceToolDefinitions } from './SpaceToolDefinitions.js'; 6 | import { fieldToolDefinitions } from './FieldToolDefinitions.js'; 7 | import { documentationToolDefinitions } from './DocumentationToolDefinitions.js'; 8 | import { layoutToolDefinitions } from './LayoutToolDefinitions.js'; 9 | import { userToolDefinitions } from './UserToolDefinitions.js'; 10 | import { systemToolDefinitions } from './SystemToolDefinitions.js'; 11 | import { fileToolDefinitions } from './FileToolDefinitions.js'; 12 | 13 | /** 14 | * 全てのツール定義をフラットな配列として提供 15 | */ 16 | export const allToolDefinitions = [ 17 | ...recordToolDefinitions, 18 | ...appToolDefinitions, 19 | ...spaceToolDefinitions, 20 | ...fieldToolDefinitions, 21 | ...documentationToolDefinitions, 22 | ...layoutToolDefinitions, 23 | ...userToolDefinitions, 24 | ...systemToolDefinitions, 25 | ...fileToolDefinitions 26 | ]; 27 | ``` -------------------------------------------------------------------------------- /src/server/tools/UserTools.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/server/tools/UserTools.js 2 | import { ValidationUtils } from '../../utils/ValidationUtils.js'; 3 | import { LoggingUtils } from '../../utils/LoggingUtils.js'; 4 | import { ResponseBuilder } from '../../utils/ResponseBuilder.js'; 5 | 6 | // ユーザー関連のツールを処理する関数 7 | export async function handleUserTools(name, args, repository) { 8 | // 共通のツール実行ログ 9 | LoggingUtils.logToolExecution('user', name, args); 10 | switch (name) { 11 | case 'get_users': { 12 | const codes = args.codes || []; 13 | 14 | if (codes.length > 0) { 15 | ValidationUtils.validateArray(codes, 'codes'); 16 | } 17 | 18 | return repository.getUsers(codes); 19 | } 20 | 21 | case 'get_groups': { 22 | const codes = args.codes || []; 23 | 24 | if (codes.length > 0) { 25 | ValidationUtils.validateArray(codes, 'codes'); 26 | } 27 | 28 | return repository.getGroups(codes); 29 | } 30 | 31 | case 'get_group_users': { 32 | ValidationUtils.validateRequired(args, ['group_code']); 33 | ValidationUtils.validateString(args.group_code, 'group_code'); 34 | 35 | return repository.getGroupUsers(args.group_code); 36 | } 37 | 38 | default: 39 | throw new Error(`Unknown user tool: ${name}`); 40 | } 41 | } 42 | ``` -------------------------------------------------------------------------------- /src/repositories/KintoneFileRepository.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/repositories/KintoneFileRepository.js 2 | import { BaseKintoneRepository } from './base/BaseKintoneRepository.js'; 3 | import { LoggingUtils } from '../utils/LoggingUtils.js'; 4 | 5 | export class KintoneFileRepository extends BaseKintoneRepository { 6 | async uploadFile(fileName, fileData) { 7 | try { 8 | LoggingUtils.logDetailedOperation('uploadFile', 'ファイルアップロード開始', { fileName }); 9 | const buffer = Buffer.from(fileData, 'base64'); 10 | const response = await this.client.file.uploadFile({ 11 | file: { 12 | name: fileName, 13 | data: buffer 14 | } 15 | }); 16 | LoggingUtils.logDetailedOperation('uploadFile', 'ファイルアップロード完了', { 17 | fileName, 18 | fileKey: response.fileKey 19 | }); 20 | return response; 21 | } catch (error) { 22 | this.handleKintoneError(error, `upload file ${fileName}`); 23 | } 24 | } 25 | 26 | async downloadFile(fileKey) { 27 | try { 28 | LoggingUtils.logDetailedOperation('downloadFile', 'ファイルダウンロード開始', { fileKey }); 29 | const response = await this.client.file.downloadFile({ fileKey: fileKey }); 30 | LoggingUtils.logDetailedOperation('downloadFile', 'ファイルダウンロード完了', { 31 | fileKey, 32 | contentType: response.contentType || 'unknown' 33 | }); 34 | return response; 35 | } catch (error) { 36 | this.handleKintoneError(error, `download file with key ${fileKey}`); 37 | } 38 | } 39 | } 40 | ``` -------------------------------------------------------------------------------- /src/server/handlers/ToolRequestHandler.js: -------------------------------------------------------------------------------- ```javascript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { ToolRouter } from './ToolRouter.js'; 3 | import { convertDropdownFieldType } from '../../utils/DataTransformers.js'; 4 | import { handleToolError } from './ErrorHandlers.js'; 5 | import { LoggingUtils } from '../../utils/LoggingUtils.js'; 6 | 7 | export async function executeToolRequest(request, repository) { 8 | const { name, arguments: args } = request.params; 9 | LoggingUtils.info('tool', 'tool_request_received', { name }); 10 | LoggingUtils.debug('tool', 'tool_request_payload', request.params); 11 | 12 | if (!name) { 13 | throw new McpError( 14 | ErrorCode.InvalidParams, 15 | `ツール名が指定されていません。` 16 | ); 17 | } 18 | 19 | if (!args) { 20 | throw new McpError( 21 | ErrorCode.InvalidParams, 22 | `ツール "${name}" の引数が指定されていません。` 23 | ); 24 | } 25 | 26 | convertDropdownFieldType(args); 27 | 28 | LoggingUtils.info('tool', 'tool_execution_start', { name }); 29 | LoggingUtils.debug('tool', 'tool_arguments', args); 30 | 31 | try { 32 | const router = new ToolRouter(); 33 | 34 | const lookupResponse = router.handleLookupFieldSpecialCase(name, args, repository); 35 | if (lookupResponse) { 36 | return await lookupResponse; 37 | } 38 | 39 | const result = await router.routeToolRequest(name, args, repository); 40 | 41 | return { 42 | content: [ 43 | { 44 | type: 'text', 45 | text: JSON.stringify(result, null, 2) 46 | } 47 | ] 48 | }; 49 | } catch (error) { 50 | return handleToolError(error); 51 | } 52 | } 53 | ``` -------------------------------------------------------------------------------- /src/server/MCPServer.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/server/MCPServer.js 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ListToolsRequestSchema, 7 | ListPromptsRequestSchema, 8 | ListResourcesRequestSchema, 9 | GetPromptRequestSchema, 10 | ListResourceTemplatesRequestSchema, 11 | ReadResourceRequestSchema, 12 | McpError, 13 | ErrorCode, 14 | } from '@modelcontextprotocol/sdk/types.js'; 15 | import { KintoneCredentials } from '../models/KintoneCredentials.js'; 16 | import { KintoneRepository } from '../repositories/KintoneRepository.js'; 17 | import { executeToolRequest } from './handlers/ToolRequestHandler.js'; 18 | import { allToolDefinitions } from './tools/definitions/index.js'; 19 | import { LoggingUtils } from '../utils/LoggingUtils.js'; 20 | 21 | export class MCPServer { 22 | constructor(domain, username, password) { 23 | this.credentials = new KintoneCredentials(domain, username, password); 24 | this.repository = new KintoneRepository(this.credentials); 25 | 26 | this.server = new Server( 27 | { 28 | name: 'kintonemcp', 29 | version: '8.0.0', 30 | }, 31 | { 32 | capabilities: { 33 | tools: {}, 34 | prompts: {}, 35 | resources: {}, 36 | }, 37 | } 38 | ); 39 | 40 | this.setupRequestHandlers(); 41 | 42 | // エラーハンドリング 43 | this.server.onerror = (error) => LoggingUtils.error('server', 'mcp_server_error', error); 44 | process.on('SIGINT', async () => { 45 | await this.server.close(); 46 | process.exit(0); 47 | }); 48 | } 49 | 50 | setupRequestHandlers() { 51 | // ツール一覧を返すハンドラー 52 | this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ 53 | tools: allToolDefinitions 54 | })); 55 | 56 | // プロンプト一覧(未提供のため空配列) 57 | this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({ 58 | prompts: [] 59 | })); 60 | this.server.setRequestHandler(GetPromptRequestSchema, async () => { 61 | throw new McpError(ErrorCode.MethodNotFound, 'promptは提供されていません'); 62 | }); 63 | 64 | // リソース一覧(未提供のため空配列) 65 | this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ 66 | resources: [] 67 | })); 68 | this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ 69 | resourceTemplates: [] 70 | })); 71 | this.server.setRequestHandler(ReadResourceRequestSchema, async () => { 72 | throw new McpError(ErrorCode.MethodNotFound, 'resourceは提供されていません'); 73 | }); 74 | 75 | // ツールリクエストを実行するハンドラー 76 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 77 | return executeToolRequest(request, this.repository); 78 | }); 79 | } 80 | 81 | async run() { 82 | const transport = new StdioServerTransport(); 83 | await this.server.connect(transport); 84 | LoggingUtils.info('server', 'mcp_server_running'); 85 | } 86 | } 87 | ``` -------------------------------------------------------------------------------- /src/server/tools/SpaceTools.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/server/tools/SpaceTools.js 2 | import { ValidationUtils } from '../../utils/ValidationUtils.js'; 3 | import { LoggingUtils } from '../../utils/LoggingUtils.js'; 4 | import { ResponseBuilder } from '../../utils/ResponseBuilder.js'; 5 | 6 | // スペース関連のツールを処理する関数 7 | export async function handleSpaceTools(name, args, repository) { 8 | // 共通のツール実行ログ 9 | LoggingUtils.logToolExecution('space', name, args); 10 | 11 | switch (name) { 12 | case 'get_space': { 13 | ValidationUtils.validateRequired(args, ['space_id']); 14 | 15 | return repository.getSpace(args.space_id); 16 | } 17 | 18 | case 'update_space': { 19 | ValidationUtils.validateRequired(args, ['space_id']); 20 | 21 | await repository.updateSpace(args.space_id, { 22 | name: args.name, 23 | isPrivate: args.isPrivate, 24 | fixedMember: args.fixedMember, 25 | useMultiThread: args.useMultiThread, 26 | }); 27 | return ResponseBuilder.success(); 28 | } 29 | 30 | case 'update_space_body': { 31 | ValidationUtils.validateRequired(args, ['space_id', 'body']); 32 | ValidationUtils.validateString(args.body, 'body'); 33 | 34 | await repository.updateSpaceBody(args.space_id, args.body); 35 | return ResponseBuilder.success(); 36 | } 37 | 38 | case 'get_space_members': { 39 | ValidationUtils.validateRequired(args, ['space_id']); 40 | 41 | return repository.getSpaceMembers(args.space_id); 42 | } 43 | 44 | case 'update_space_members': { 45 | ValidationUtils.validateRequired(args, ['space_id', 'members']); 46 | ValidationUtils.validateArray(args.members, 'members'); 47 | 48 | await repository.updateSpaceMembers(args.space_id, args.members); 49 | return ResponseBuilder.success(); 50 | } 51 | 52 | case 'add_thread': { 53 | ValidationUtils.validateRequired(args, ['space_id', 'name']); 54 | ValidationUtils.validateString(args.name, 'name'); 55 | 56 | const response = await repository.addThread(args.space_id, args.name); 57 | return ResponseBuilder.withId('thread_id', response.id); 58 | } 59 | 60 | case 'update_thread': { 61 | ValidationUtils.validateRequired(args, ['thread_id']); 62 | 63 | await repository.updateThread(args.thread_id, { 64 | name: args.name, 65 | body: args.body, 66 | }); 67 | return ResponseBuilder.success(); 68 | } 69 | 70 | case 'add_thread_comment': { 71 | ValidationUtils.validateRequired(args, ['space_id', 'thread_id', 'text']); 72 | ValidationUtils.validateString(args.text, 'text'); 73 | 74 | const response = await repository.addThreadComment( 75 | args.space_id, 76 | args.thread_id, 77 | { 78 | text: args.text, 79 | mentions: args.mentions || [], 80 | } 81 | ); 82 | return ResponseBuilder.withId('comment_id', response.id); 83 | } 84 | 85 | case 'add_guests': { 86 | ValidationUtils.validateRequired(args, ['guests']); 87 | ValidationUtils.validateArray(args.guests, 'guests', { minLength: 1 }); 88 | 89 | await repository.addGuests(args.guests); 90 | return ResponseBuilder.success(); 91 | } 92 | 93 | case 'update_space_guests': { 94 | ValidationUtils.validateRequired(args, ['space_id', 'guests']); 95 | ValidationUtils.validateArray(args.guests, 'guests'); 96 | 97 | await repository.updateSpaceGuests(args.space_id, args.guests); 98 | return ResponseBuilder.success(); 99 | } 100 | 101 | default: 102 | throw new Error(`Unknown space tool: ${name}`); 103 | } 104 | } ``` -------------------------------------------------------------------------------- /src/repositories/KintoneSpaceRepository.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/repositories/KintoneSpaceRepository.js 2 | import { BaseKintoneRepository } from './base/BaseKintoneRepository.js'; 3 | import { LoggingUtils } from '../utils/LoggingUtils.js'; 4 | import { ResponseBuilder } from '../utils/ResponseBuilder.js'; 5 | 6 | export class KintoneSpaceRepository extends BaseKintoneRepository { 7 | async getSpace(spaceId) { 8 | try { 9 | LoggingUtils.logDetailedOperation('getSpace', 'スペース情報取得', { spaceId }); 10 | const response = await this.client.space.getSpace({ id: spaceId }); 11 | LoggingUtils.logDetailedOperation('getSpace', 'スペース情報取得完了', { spaceId }); 12 | return response; 13 | } catch (error) { 14 | this.handleKintoneError(error, `get space ${spaceId}`); 15 | } 16 | } 17 | 18 | async updateSpace(spaceId, settings) { 19 | try { 20 | LoggingUtils.logDetailedOperation('updateSpace', 'スペース情報更新', { spaceId, settings }); 21 | await this.client.space.updateSpace({ 22 | id: spaceId, 23 | ...settings 24 | }); 25 | LoggingUtils.logDetailedOperation('updateSpace', 'スペース情報更新完了', { spaceId }); 26 | } catch (error) { 27 | this.handleKintoneError(error, `update space ${spaceId}`); 28 | } 29 | } 30 | 31 | async updateSpaceBody(spaceId, body) { 32 | try { 33 | LoggingUtils.logDetailedOperation('updateSpaceBody', 'スペース本文更新', { spaceId, bodyLength: body.length }); 34 | await this.client.space.updateSpaceBody({ 35 | id: spaceId, 36 | body: body 37 | }); 38 | LoggingUtils.logDetailedOperation('updateSpaceBody', 'スペース本文更新完了', { spaceId }); 39 | } catch (error) { 40 | this.handleKintoneError(error, `update space body ${spaceId}`); 41 | } 42 | } 43 | 44 | async getSpaceMembers(spaceId) { 45 | try { 46 | LoggingUtils.logDetailedOperation('getSpaceMembers', 'スペースメンバー取得', { spaceId }); 47 | const response = await this.client.space.getSpaceMembers({ id: spaceId }); 48 | LoggingUtils.logDetailedOperation('getSpaceMembers', 'スペースメンバー取得完了', { 49 | spaceId, 50 | memberCount: response.members ? response.members.length : 0 51 | }); 52 | return response; 53 | } catch (error) { 54 | this.handleKintoneError(error, `get space members ${spaceId}`); 55 | } 56 | } 57 | 58 | async updateSpaceMembers(spaceId, members) { 59 | try { 60 | LoggingUtils.logDetailedOperation('updateSpaceMembers', 'スペースメンバー更新', { 61 | spaceId, 62 | memberCount: members.length 63 | }); 64 | await this.client.space.updateSpaceMembers({ 65 | id: spaceId, 66 | members: members 67 | }); 68 | LoggingUtils.logDetailedOperation('updateSpaceMembers', 'スペースメンバー更新完了', { spaceId }); 69 | } catch (error) { 70 | this.handleKintoneError(error, `update space members ${spaceId}`); 71 | } 72 | } 73 | 74 | async addThread(spaceId, name) { 75 | try { 76 | LoggingUtils.logDetailedOperation('addThread', 'スレッド作成', { spaceId, threadName: name }); 77 | const response = await this.client.space.addThread({ 78 | space: spaceId, 79 | name: name 80 | }); 81 | LoggingUtils.logDetailedOperation('addThread', 'スレッド作成完了', { 82 | spaceId, 83 | threadId: response.id 84 | }); 85 | return response; 86 | } catch (error) { 87 | this.handleKintoneError(error, `add thread to space ${spaceId}`); 88 | } 89 | } 90 | 91 | async updateThread(threadId, params) { 92 | try { 93 | LoggingUtils.logDetailedOperation('updateThread', 'スレッド更新', { threadId, params }); 94 | await this.client.space.updateThread({ 95 | id: threadId, 96 | ...params 97 | }); 98 | LoggingUtils.logDetailedOperation('updateThread', 'スレッド更新完了', { threadId }); 99 | } catch (error) { 100 | this.handleKintoneError(error, `update thread ${threadId}`); 101 | } 102 | } 103 | 104 | async addThreadComment(spaceId, threadId, comment) { 105 | try { 106 | LoggingUtils.logDetailedOperation('addThreadComment', 'コメント追加', { 107 | spaceId, 108 | threadId, 109 | commentLength: comment.text ? comment.text.length : 0 110 | }); 111 | const response = await this.client.space.addThreadComment({ 112 | space: spaceId, 113 | thread: threadId, 114 | comment: comment 115 | }); 116 | LoggingUtils.logDetailedOperation('addThreadComment', 'コメント追加完了', { 117 | commentId: response.id 118 | }); 119 | return response; 120 | } catch (error) { 121 | this.handleKintoneError(error, `add comment to thread ${threadId}`); 122 | } 123 | } 124 | 125 | async updateSpaceGuests(spaceId, guests) { 126 | try { 127 | LoggingUtils.logDetailedOperation('updateSpaceGuests', 'スペースゲスト更新', { 128 | spaceId, 129 | guestCount: guests.length 130 | }); 131 | await this.client.space.updateSpaceGuests({ 132 | id: spaceId, 133 | guests: guests 134 | }); 135 | LoggingUtils.logDetailedOperation('updateSpaceGuests', 'スペースゲスト更新完了', { spaceId }); 136 | } catch (error) { 137 | this.handleKintoneError(error, `update space guests ${spaceId}`); 138 | } 139 | } 140 | } 141 | ``` -------------------------------------------------------------------------------- /src/server/handlers/ToolRouter.js: -------------------------------------------------------------------------------- ```javascript 1 | import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; 2 | import { handleRecordTools } from '../tools/RecordTools.js'; 3 | import { handleAppTools } from '../tools/AppTools.js'; 4 | import { handleSpaceTools } from '../tools/SpaceTools.js'; 5 | import { handleFieldTools } from '../tools/FieldTools.js'; 6 | import { handleDocumentationTools } from '../tools/DocumentationTools.js'; 7 | import { handleLayoutTools } from '../tools/LayoutTools.js'; 8 | import { handleUserTools } from '../tools/UserTools.js'; 9 | import { handleSystemTools } from '../tools/SystemTools.js'; 10 | import { handleFileTools } from '../tools/FileTools.js'; 11 | import { LoggingUtils } from '../../utils/LoggingUtils.js'; 12 | 13 | export class ToolRouter { 14 | constructor() { 15 | this.toolCategories = { 16 | record: { 17 | tools: ['get_record', 'search_records', 'create_record', 'update_record', 'add_record_comment', 18 | 'update_record_status', 'update_record_assignees', 19 | 'get_record_comments', 'update_record_comment', 'create_records', 'upsert_record', 20 | 'upsert_records'], 21 | handler: handleRecordTools 22 | }, 23 | app: { 24 | tools: [ 25 | 'create_app', 'deploy_app', 'get_deploy_status', 'update_app_settings', 'get_apps_info', 26 | 'get_form_layout', 'get_form_fields', 'update_form_layout', 'get_preview_app_settings', 27 | 'get_preview_form_fields', 'get_preview_form_layout', 28 | 'move_app_to_space', 'move_app_from_space', 'get_app_actions', 'get_app_plugins', 29 | 'get_process_management', 'update_process_management', 'get_views', 'update_views', 'get_app_acl', 30 | 'get_field_acl', 'update_field_acl', 'get_reports', 'update_reports', 31 | 'get_notifications', 'update_notifications', 'get_per_record_notifications', 'update_per_record_notifications', 32 | 'get_reminder_notifications', 'update_reminder_notifications', 'update_app_actions', 'update_plugins', 33 | 'get_app_customize', 'update_app_customize', 'update_app_acl', 'get_record_acl', 'evaluate_records_acl' 34 | ], 35 | handler: handleAppTools 36 | }, 37 | space: { 38 | tools: [ 39 | 'get_space', 'update_space', 'update_space_body', 'get_space_members', 40 | 'update_space_members', 'add_thread', 'update_thread', 'add_thread_comment', 41 | 'add_guests', 'update_space_guests' 42 | ], 43 | handler: handleSpaceTools 44 | }, 45 | field: { 46 | tools: [ 47 | 'add_fields', 'update_field', 'create_choice_field', 'create_reference_table_field', 48 | 'create_text_field', 'create_number_field', 'create_date_field', 'create_time_field', 49 | 'create_datetime_field', 'create_rich_text_field', 'create_attachment_field', 50 | 'create_user_select_field', 'create_subtable_field', 'create_calc_field', 51 | 'create_status_field', 'create_related_records_field', 'create_link_field' 52 | ], 53 | handler: handleFieldTools 54 | }, 55 | documentation: { 56 | tools: [ 57 | 'get_field_type_documentation', 'get_available_field_types', 58 | 'get_documentation_tool_description', 'get_field_creation_tool_description', 59 | 'get_group_element_structure', 'get_query_language_documentation' 60 | ], 61 | handler: handleDocumentationTools 62 | }, 63 | file: { 64 | tools: ['upload_file', 'download_file'], 65 | handler: handleFileTools 66 | }, 67 | layout: { 68 | tools: [ 69 | 'create_form_layout', 'update_form_layout', 'add_layout_element', 70 | 'create_group_layout', 'create_table_layout' 71 | ], 72 | handler: handleLayoutTools 73 | }, 74 | user: { 75 | tools: ['get_users', 'get_groups', 'get_group_users'], 76 | handler: handleUserTools 77 | }, 78 | system: { 79 | tools: ['get_kintone_domain', 'get_kintone_username'], 80 | handler: handleSystemTools 81 | } 82 | }; 83 | } 84 | 85 | findToolCategory(toolName) { 86 | for (const [categoryName, category] of Object.entries(this.toolCategories)) { 87 | if (category.tools.includes(toolName)) { 88 | return { categoryName, handler: category.handler }; 89 | } 90 | } 91 | return null; 92 | } 93 | 94 | async routeToolRequest(toolName, args, repository) { 95 | const toolCategory = this.findToolCategory(toolName); 96 | 97 | if (!toolCategory) { 98 | throw new McpError( 99 | ErrorCode.MethodNotFound, 100 | `Unknown tool: ${toolName}` 101 | ); 102 | } 103 | 104 | return await toolCategory.handler(toolName, args, repository); 105 | } 106 | 107 | handleLookupFieldSpecialCase(toolName, args, repository) { 108 | if (toolName !== 'create_lookup_field') { 109 | return null; 110 | } 111 | 112 | return this.createLookupFieldResponse(args, repository); 113 | } 114 | 115 | async createLookupFieldResponse(args, repository) { 116 | const fieldConfig = await handleFieldTools('create_lookup_field', args, repository); 117 | 118 | const note = `注意: create_lookup_field ツールは設定オブジェクトを生成するだけのヘルパーツールです。実際にフィールドを追加するには、この結果を add_fields ツールに渡してください。`; 119 | const lookupNote = ` 120 | 【重要】ルックアップフィールドについて 121 | - ルックアップフィールドは基本的なフィールドタイプ(SINGLE_LINE_TEXT、NUMBERなど)に、lookup属性を追加したものです 122 | - フィールドタイプとして "LOOKUP" を指定するのではなく、適切な基本タイプを指定し、その中にlookupプロパティを設定します 123 | - 参照先アプリは運用環境にデプロイされている必要があります 124 | - ルックアップのキーフィールド自体はフィールドマッピングに含めないでください 125 | - lookupPickerFieldsとsortは省略可能ですが、指定することを強く推奨します 126 | `; 127 | const example = `使用例: 128 | add_fields({ 129 | app_id: アプリID, 130 | properties: { 131 | "${fieldConfig.code}": ${JSON.stringify(fieldConfig, null, 2)} 132 | } 133 | });`; 134 | LoggingUtils.info('field', 'lookup_field_guidance_generated', { fieldCode: fieldConfig.code }); 135 | 136 | const result = { 137 | ...fieldConfig, 138 | _note: note, 139 | _lookupNote: lookupNote, 140 | _example: example 141 | }; 142 | 143 | return { 144 | content: [ 145 | { 146 | type: 'text', 147 | text: JSON.stringify(result, null, 2) 148 | }, 149 | { 150 | type: 'text', 151 | text: note 152 | }, 153 | { 154 | type: 'text', 155 | text: lookupNote 156 | }, 157 | { 158 | type: 'text', 159 | text: example 160 | } 161 | ] 162 | }; 163 | } 164 | } 165 | ``` -------------------------------------------------------------------------------- /src/repositories/base/http/KintoneHttpClient.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/repositories/base/http/KintoneHttpClient.js 2 | import { KintoneApiError } from './KintoneApiError.js'; 3 | 4 | const DEFAULT_TIMEOUT_MS = 60000; 5 | 6 | function buildPath({ endpointName, guestSpaceId, preview = false }) { 7 | const guestPath = guestSpaceId !== undefined && guestSpaceId !== null 8 | ? `/guest/${guestSpaceId}` 9 | : ''; 10 | const previewPath = preview ? '/preview' : ''; 11 | return `/k${guestPath}/v1${previewPath}/${endpointName}.json`; 12 | } 13 | 14 | const FORM_DATA_SUPPORTED = typeof FormData !== 'undefined'; 15 | 16 | function isFormData(value) { 17 | return FORM_DATA_SUPPORTED && value instanceof FormData; 18 | } 19 | 20 | function stripUndefined(value) { 21 | if (value === null || value === undefined) { 22 | return undefined; 23 | } 24 | if (Array.isArray(value)) { 25 | return value.map(stripUndefined); 26 | } 27 | if (typeof value === 'object' && !isFormData(value) && !(value instanceof Date) && !(value instanceof Buffer)) { 28 | const result = {}; 29 | for (const [key, entry] of Object.entries(value)) { 30 | if (entry !== undefined) { 31 | const cleaned = stripUndefined(entry); 32 | if (cleaned !== undefined) { 33 | result[key] = cleaned; 34 | } 35 | } 36 | } 37 | return result; 38 | } 39 | return value; 40 | } 41 | 42 | export class KintoneHttpClient { 43 | constructor(credentials) { 44 | this.credentials = credentials; 45 | this.baseUrl = `https://${credentials.domain}`; 46 | this.authHeader = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64'); 47 | } 48 | 49 | getDefaultHeaders() { 50 | return { 51 | 'X-Cybozu-Authorization': this.authHeader, 52 | 'X-Requested-With': 'XMLHttpRequest', 53 | 'Accept': 'application/json' 54 | }; 55 | } 56 | 57 | async request(method, endpointName, { params, data, preview = false, headers = {}, responseType, rawResponse = false } = {}) { 58 | const path = buildPath({ 59 | endpointName, 60 | guestSpaceId: this.credentials.guestSpaceId, 61 | preview 62 | }); 63 | const url = new URL(path, this.baseUrl); 64 | 65 | const cleanedParams = params ? stripUndefined(params) : undefined; 66 | if (cleanedParams) { 67 | for (const [key, value] of Object.entries(cleanedParams)) { 68 | if (value === undefined || value === null) { 69 | continue; 70 | } 71 | if (Array.isArray(value)) { 72 | for (const item of value) { 73 | if (item !== undefined && item !== null) { 74 | url.searchParams.append(key, String(item)); 75 | } 76 | } 77 | } else { 78 | url.searchParams.append(key, String(value)); 79 | } 80 | } 81 | } 82 | 83 | const requestHeaders = new Headers(this.getDefaultHeaders()); 84 | for (const [key, value] of Object.entries(headers)) { 85 | if (value !== undefined && value !== null) { 86 | requestHeaders.set(key, String(value)); 87 | } 88 | } 89 | 90 | const init = { 91 | method: method.toUpperCase(), 92 | headers: requestHeaders, 93 | signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS) 94 | }; 95 | 96 | const expectedResponseType = responseType || 'json'; 97 | 98 | if (data !== undefined) { 99 | if (isFormData(data)) { 100 | // fetchがboundary付きヘッダーを自動付与するため、Content-Typeは削除 101 | requestHeaders.delete('Content-Type'); 102 | init.body = data; 103 | } else { 104 | const payload = stripUndefined(data); 105 | init.body = JSON.stringify(payload); 106 | if (!requestHeaders.has('Content-Type')) { 107 | requestHeaders.set('Content-Type', 'application/json'); 108 | } 109 | } 110 | } 111 | 112 | let response; 113 | try { 114 | response = await fetch(url, init); 115 | } catch (error) { 116 | if (error instanceof KintoneApiError) { 117 | throw error; 118 | } 119 | const isAbortError = error?.name === 'AbortError'; 120 | throw new KintoneApiError( 121 | isAbortError ? 'kintone API request timed out' : 'kintone API request failed without response', 122 | { 123 | status: null, 124 | responseBody: null 125 | } 126 | ); 127 | } 128 | 129 | const parseErrorBody = async () => { 130 | const contentType = response.headers.get('content-type') || ''; 131 | if (contentType.includes('application/json')) { 132 | try { 133 | return await response.json(); 134 | } catch (parseError) { 135 | return null; 136 | } 137 | } 138 | try { 139 | return await response.text(); 140 | } catch (parseError) { 141 | return null; 142 | } 143 | }; 144 | 145 | if (!response.ok) { 146 | const body = await parseErrorBody(); 147 | const message = typeof body === 'object' && body?.message 148 | ? body.message 149 | : `kintone API request failed with status ${response.status}`; 150 | 151 | throw new KintoneApiError(message, { 152 | status: response.status, 153 | code: typeof body === 'object' ? body?.code : undefined, 154 | errors: typeof body === 'object' ? body?.errors : undefined, 155 | responseBody: body 156 | }); 157 | } 158 | 159 | if (rawResponse) { 160 | const dataBuffer = expectedResponseType === 'arraybuffer' 161 | ? Buffer.from(await response.arrayBuffer()) 162 | : await response.text(); 163 | return { 164 | data: dataBuffer, 165 | headers: Object.fromEntries(response.headers.entries()), 166 | status: response.status 167 | }; 168 | } 169 | 170 | if (expectedResponseType === 'arraybuffer') { 171 | return Buffer.from(await response.arrayBuffer()); 172 | } 173 | 174 | if (expectedResponseType === 'text') { 175 | return await response.text(); 176 | } 177 | 178 | if (response.status === 204) { 179 | return null; 180 | } 181 | 182 | const text = await response.text(); 183 | if (!text) { 184 | return {}; 185 | } 186 | 187 | try { 188 | return JSON.parse(text); 189 | } catch (error) { 190 | throw new KintoneApiError('kintone API returned invalid JSON response', { 191 | status: response.status, 192 | responseBody: text 193 | }); 194 | } 195 | } 196 | 197 | get(endpointName, params, options = {}) { 198 | return this.request('get', endpointName, { ...options, params }); 199 | } 200 | 201 | post(endpointName, data, options = {}) { 202 | return this.request('post', endpointName, { ...options, data }); 203 | } 204 | 205 | put(endpointName, data, options = {}) { 206 | return this.request('put', endpointName, { ...options, data }); 207 | } 208 | 209 | delete(endpointName, data, options = {}) { 210 | return this.request('delete', endpointName, { ...options, data }); 211 | } 212 | } 213 | ``` -------------------------------------------------------------------------------- /src/repositories/KintoneRecordRepository.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/repositories/KintoneRecordRepository.js 2 | import { BaseKintoneRepository } from './base/BaseKintoneRepository.js'; 3 | import { KintoneRecord } from '../models/KintoneRecord.js'; 4 | import { LoggingUtils } from '../utils/LoggingUtils.js'; 5 | 6 | export class KintoneRecordRepository extends BaseKintoneRepository { 7 | async getRecord(appId, recordId) { 8 | const params = { app: appId, id: recordId }; 9 | return this.executeWithDetailedLogging( 10 | 'getRecord', 11 | params, 12 | () => this.client.record.getRecord(params), 13 | `get record ${appId}/${recordId}` 14 | ).then(response => new KintoneRecord(appId, recordId, response.record)); 15 | } 16 | 17 | async searchRecords(appId, query, fields = []) { 18 | const params = { app: appId }; 19 | 20 | // クエリ文字列の処理 21 | if (query) { 22 | // クエリ文字列が order や limit のみで構成されているかチェック 23 | // 条件句の検出: 比較/文字列/集合/空判定(is empty / is not empty)をサポート 24 | const hasCondition = /[^\s]+(?:\s*(?:=|!=|>|<|>=|<=|like|in|not\s+in)|\s+is\s+(?:not\s+)?empty)/i.test(query); 25 | const hasOrderOrLimit = /(order |limit )/i.test(query); 26 | 27 | // order や limit のみの場合、$id > 0 を先頭に挿入 28 | if (!hasCondition && hasOrderOrLimit) { 29 | params.query = `$id > 0 ${query}`; 30 | LoggingUtils.logOperation('Modified query', params.query); 31 | } else { 32 | params.query = query; 33 | } 34 | } 35 | 36 | if (fields.length > 0) { 37 | params.fields = fields; 38 | } 39 | 40 | // getAllRecords ではなく getRecords を使用 41 | return this.executeWithDetailedLogging( 42 | 'searchRecords', 43 | params, 44 | () => this.client.record.getRecords(params), 45 | `search records ${appId}` 46 | ).then(response => { 47 | const records = response.records || []; 48 | LoggingUtils.logOperation(`Found records`, `${records.length} records`); 49 | return records.map((record) => { 50 | const recordId = record.$id?.value || 'unknown'; 51 | return new KintoneRecord(appId, recordId, record); 52 | }); 53 | }); 54 | } 55 | 56 | async createRecord(appId, fields) { 57 | const params = { app: appId, record: fields }; 58 | return this.executeWithDetailedLogging( 59 | 'createRecord', 60 | params, 61 | () => this.client.record.addRecord(params), 62 | `create record in app ${appId}` 63 | ).then(response => response.id); 64 | } 65 | 66 | async updateRecord(record) { 67 | const params = { 68 | app: record.appId, 69 | id: record.recordId, 70 | record: record.fields 71 | }; 72 | return this.executeWithDetailedLogging( 73 | 'updateRecord', 74 | params, 75 | () => this.client.record.updateRecord(params), 76 | `update record ${record.appId}/${record.recordId}` 77 | ); 78 | } 79 | 80 | async updateRecordByKey(appId, keyField, keyValue, fields) { 81 | const params = { 82 | app: appId, 83 | updateKey: { 84 | field: keyField, 85 | value: keyValue 86 | }, 87 | record: fields 88 | }; 89 | return this.executeWithDetailedLogging( 90 | 'updateRecordByKey', 91 | params, 92 | () => this.client.record.updateRecordByUpdateKey(params), 93 | `update record by key ${appId}/${keyField}=${keyValue}` 94 | ); 95 | } 96 | 97 | async addRecordComment(appId, recordId, text, mentions = []) { 98 | const params = { 99 | app: appId, 100 | record: recordId, 101 | comment: { 102 | text: text, 103 | mentions: mentions 104 | } 105 | }; 106 | return this.executeWithDetailedLogging( 107 | 'addRecordComment', 108 | params, 109 | () => this.client.record.addRecordComment(params), 110 | `add comment to record ${appId}/${recordId}` 111 | ).then(response => response.id); 112 | } 113 | 114 | async getRecordAcl(appId, recordIds) { 115 | // kintone REST APIではレコード単位のアクセス権限取得はサポートされていません 116 | throw new Error( 117 | 'レコード単位のアクセス権限取得機能はkintone REST APIではサポートされていません。\n\n' + 118 | '代替案:\n' + 119 | '1. アプリ全体のアクセス権限を確認する場合は get_app_acl を使用してください\n' + 120 | '2. プロセス管理のステータスに基づくアクセス制御を確認する場合は get_process_management を使用してください\n' + 121 | '3. レコードの作成者・更新者を確認する場合は、レコード取得時に CREATOR/MODIFIER フィールドを参照してください' 122 | ); 123 | } 124 | 125 | async updateRecordStatus(appId, recordId, action, assignee = null) { 126 | const params = { 127 | app: appId, 128 | id: recordId, 129 | action: action 130 | }; 131 | 132 | if (assignee) { 133 | params.assignee = assignee; 134 | } 135 | 136 | return this.executeWithDetailedLogging( 137 | 'updateRecordStatus', 138 | params, 139 | () => this.client.record.updateRecordStatus(params), 140 | `update record status ${appId}/${recordId}` 141 | ); 142 | } 143 | 144 | async updateRecordAssignees(appId, recordId, assignees) { 145 | const params = { 146 | app: appId, 147 | id: recordId, 148 | assignees: assignees 149 | }; 150 | return this.executeWithDetailedLogging( 151 | 'updateRecordAssignees', 152 | params, 153 | () => this.client.record.updateRecordAssignees(params), 154 | `update record assignees ${appId}/${recordId}` 155 | ); 156 | } 157 | 158 | async getRecordComments(appId, recordId, order = 'desc', offset = 0, limit = 10) { 159 | const params = { 160 | app: appId, 161 | record: recordId, 162 | order: order, 163 | offset: offset, 164 | limit: limit 165 | }; 166 | return this.executeWithDetailedLogging( 167 | 'getRecordComments', 168 | params, 169 | () => this.client.record.getRecordComments(params), 170 | `get comments for record ${appId}/${recordId}` 171 | ).then(response => ({ 172 | comments: response.comments, 173 | totalCount: response.totalCount 174 | })); 175 | } 176 | 177 | async updateRecordComment(appId, recordId, commentId, text, mentions = []) { 178 | const params = { 179 | app: appId, 180 | record: recordId, 181 | comment: commentId, 182 | text: text, 183 | mentions: mentions 184 | }; 185 | return this.executeWithDetailedLogging( 186 | 'updateRecordComment', 187 | params, 188 | () => this.client.record.updateRecordComment(params), 189 | `update comment ${commentId} for record ${appId}/${recordId}` 190 | ); 191 | } 192 | 193 | async createRecords(appId, records) { 194 | const params = { 195 | app: appId, 196 | records: records 197 | }; 198 | return this.executeWithDetailedLogging( 199 | 'createRecords', 200 | params, 201 | () => this.client.record.addRecords(params), 202 | `create records in app ${appId}` 203 | ); 204 | } 205 | 206 | async updateRecords(appId, records) { 207 | const params = { 208 | app: appId, 209 | records: records 210 | }; 211 | return this.executeWithDetailedLogging( 212 | 'updateRecords', 213 | params, 214 | () => this.client.record.updateRecords(params), 215 | `update records in app ${appId}` 216 | ); 217 | } 218 | 219 | async upsertRecord(appId, updateKey, fields) { 220 | const params = { 221 | app: appId, 222 | updateKey: updateKey, 223 | record: fields 224 | }; 225 | return this.executeWithDetailedLogging( 226 | 'upsertRecord', 227 | params, 228 | () => this.client.record.upsertRecord(params), 229 | `upsert record in app ${appId} with key ${updateKey.field}=${updateKey.value}` 230 | ); 231 | } 232 | 233 | async upsertRecords(appId, records) { 234 | const params = { 235 | app: appId, 236 | records: records.map((entry) => ({ 237 | updateKey: entry.updateKey, 238 | record: entry.fields 239 | })) 240 | }; 241 | 242 | return this.executeWithDetailedLogging( 243 | 'upsertRecords', 244 | params, 245 | () => this.client.record.upsertRecords(params), 246 | `upsert multiple records in app ${appId}` 247 | ); 248 | } 249 | } 250 | ``` -------------------------------------------------------------------------------- /src/server/tools/RecordTools.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/server/tools/RecordTools.js 2 | import { KintoneRecord } from '../../models/KintoneRecord.js'; 3 | import { ValidationUtils } from '../../utils/ValidationUtils.js'; 4 | import { LoggingUtils } from '../../utils/LoggingUtils.js'; 5 | import { ResponseBuilder } from '../../utils/ResponseBuilder.js'; 6 | 7 | // レコード関連のツールを処理する関数 8 | export async function handleRecordTools(name, args, repository) { 9 | // 共通のツール実行ログ 10 | LoggingUtils.logToolExecution('record', name, args); 11 | 12 | switch (name) { 13 | case 'get_record': { 14 | ValidationUtils.validateRequired(args, ['app_id', 'record_id']); 15 | 16 | const record = await repository.getRecord(args.app_id, args.record_id); 17 | return record.fields; // KintoneRecord ではなく fields を返す 18 | } 19 | 20 | case 'search_records': { 21 | ValidationUtils.validateRequired(args, ['app_id']); 22 | 23 | // クエリーが提供されている場合、kintoneクエリー構文を検証 24 | if (args.query) { 25 | ValidationUtils.validateKintoneQuery(args.query); 26 | } 27 | 28 | const records = await repository.searchRecords( 29 | args.app_id, 30 | args.query, 31 | args.fields 32 | ); 33 | return records.map(r => r.fields); 34 | } 35 | 36 | case 'create_record': { 37 | ValidationUtils.validateRequired(args, ['app_id', 'fields']); 38 | ValidationUtils.validateObject(args.fields, 'fields'); 39 | 40 | // フィールドの検証 41 | if (!args.fields.project_manager) { 42 | LoggingUtils.logWarning('create_record', 'project_manager field is missing'); 43 | } 44 | 45 | const recordId = await repository.createRecord( 46 | args.app_id, 47 | args.fields 48 | ); 49 | return ResponseBuilder.recordCreated(recordId); 50 | } 51 | 52 | case 'update_record': { 53 | ValidationUtils.validateRequired(args, ['app_id', 'fields']); 54 | 55 | // レコードIDまたはupdateKeyのいずれかが必要 56 | if (!args.record_id && !args.updateKey) { 57 | throw new Error('record_id または updateKey は必須パラメータです。'); 58 | } 59 | 60 | ValidationUtils.validateObject(args.fields, 'fields'); 61 | 62 | let response; 63 | if (args.record_id) { 64 | // レコードIDを使用した更新 65 | response = await repository.updateRecord( 66 | new KintoneRecord( 67 | args.app_id, 68 | args.record_id, 69 | args.fields 70 | ) 71 | ); 72 | } else { 73 | // updateKeyを使用した更新 74 | response = await repository.updateRecordByKey( 75 | args.app_id, 76 | args.updateKey.field, 77 | args.updateKey.value, 78 | args.fields 79 | ); 80 | } 81 | 82 | return ResponseBuilder.recordUpdated(response.revision); 83 | } 84 | 85 | case 'add_record_comment': { 86 | ValidationUtils.validateRequired(args, ['app_id', 'record_id', 'text']); 87 | ValidationUtils.validateString(args.text, 'text'); 88 | 89 | const commentId = await repository.addRecordComment( 90 | args.app_id, 91 | args.record_id, 92 | args.text, 93 | args.mentions || [] 94 | ); 95 | return ResponseBuilder.withId('comment_id', commentId); 96 | } 97 | 98 | case 'update_record_status': { 99 | ValidationUtils.validateRequired(args, ['app_id', 'record_id', 'action']); 100 | ValidationUtils.validateString(args.action, 'action'); 101 | 102 | const response = await repository.updateRecordStatus( 103 | args.app_id, 104 | args.record_id, 105 | args.action, 106 | args.assignee 107 | ); 108 | return ResponseBuilder.withRevision(response.revision); 109 | } 110 | 111 | case 'update_record_assignees': { 112 | ValidationUtils.validateRequired(args, ['app_id', 'record_id', 'assignees']); 113 | ValidationUtils.validateArray(args.assignees, 'assignees', { 114 | maxLength: 100 115 | }); 116 | 117 | const response = await repository.updateRecordAssignees( 118 | args.app_id, 119 | args.record_id, 120 | args.assignees 121 | ); 122 | return ResponseBuilder.withRevision(response.revision); 123 | } 124 | 125 | case 'get_record_comments': { 126 | ValidationUtils.validateRequired(args, ['app_id', 'record_id']); 127 | 128 | const comments = await repository.getRecordComments( 129 | args.app_id, 130 | args.record_id, 131 | args.order || 'desc', 132 | args.offset || 0, 133 | args.limit || 10 134 | ); 135 | return comments; 136 | } 137 | 138 | case 'update_record_comment': { 139 | ValidationUtils.validateRequired(args, ['app_id', 'record_id', 'comment_id', 'text']); 140 | ValidationUtils.validateString(args.text, 'text'); 141 | 142 | await repository.updateRecordComment( 143 | args.app_id, 144 | args.record_id, 145 | args.comment_id, 146 | args.text, 147 | args.mentions || [] 148 | ); 149 | return ResponseBuilder.success(); 150 | } 151 | 152 | case 'create_records': { 153 | ValidationUtils.validateRequired(args, ['app_id', 'records']); 154 | ValidationUtils.validateArray(args.records, 'records', { 155 | minLength: 1, 156 | maxLength: 100 157 | }); 158 | 159 | const result = await repository.createRecords(args.app_id, args.records); 160 | return ResponseBuilder.recordsCreated(result.ids, result.revisions); 161 | } 162 | 163 | case 'upsert_record': { 164 | ValidationUtils.validateRequired(args, ['app_id', 'updateKey', 'fields']); 165 | ValidationUtils.validateObject(args.updateKey, 'updateKey'); 166 | ValidationUtils.validateString(args.updateKey.field, 'updateKey.field'); 167 | ValidationUtils.validateObject(args.fields, 'fields'); 168 | 169 | const result = await repository.upsertRecord( 170 | args.app_id, 171 | args.updateKey, 172 | args.fields 173 | ); 174 | 175 | // result.id: 作成または更新されたレコードのID 176 | // result.revision: レコードのリビジョン番号 177 | return { 178 | record_id: result.id, 179 | revision: result.revision, 180 | message: `レコードをupsertしました (ID: ${result.id})` 181 | }; 182 | } 183 | 184 | case 'upsert_records': { 185 | ValidationUtils.validateRequired(args, ['app_id', 'records']); 186 | ValidationUtils.validateArray(args.records, 'records', { 187 | minLength: 1, 188 | maxLength: 100 189 | }); 190 | 191 | const normalizedRecords = args.records.map((entry, index) => { 192 | ValidationUtils.validateObject(entry, `records[${index}]`); 193 | ValidationUtils.validateObject(entry.updateKey, `records[${index}].updateKey`); 194 | ValidationUtils.validateString(entry.updateKey.field, `records[${index}].updateKey.field`); 195 | if (entry.updateKey.value === undefined || entry.updateKey.value === null) { 196 | throw new Error(`records[${index}].updateKey.value は必須です。`); 197 | } 198 | ValidationUtils.validateObject(entry.fields, `records[${index}].fields`); 199 | 200 | return { 201 | updateKey: { 202 | field: entry.updateKey.field, 203 | value: entry.updateKey.value 204 | }, 205 | fields: entry.fields 206 | }; 207 | }); 208 | 209 | const result = await repository.upsertRecords(args.app_id, normalizedRecords); 210 | return ResponseBuilder.recordsUpserted(result, { 211 | message: `${result.length}件のレコードをupsertしました` 212 | }); 213 | } 214 | 215 | default: 216 | throw new Error(`Unknown record tool: ${name}`); 217 | } 218 | } 219 | ``` -------------------------------------------------------------------------------- /src/repositories/KintoneRepository.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/repositories/KintoneRepository.js 2 | import { KintoneRecordRepository } from './KintoneRecordRepository.js'; 3 | import { KintoneAppRepository } from './KintoneAppRepository.js'; 4 | import { KintoneFileRepository } from './KintoneFileRepository.js'; 5 | import { KintoneSpaceRepository } from './KintoneSpaceRepository.js'; 6 | import { KintoneUserRepository } from './KintoneUserRepository.js'; 7 | 8 | export class KintoneRepository { 9 | constructor(credentials) { 10 | this.credentials = credentials; 11 | 12 | // 各専門リポジトリのインスタンスを作成 13 | this.recordRepo = new KintoneRecordRepository(credentials); 14 | this.appRepo = new KintoneAppRepository(credentials); 15 | this.fileRepo = new KintoneFileRepository(credentials); 16 | this.spaceRepo = new KintoneSpaceRepository(credentials); 17 | this.userRepo = new KintoneUserRepository(credentials); 18 | } 19 | 20 | // プレビュー環境のアプリ設定を取得 21 | async getPreviewAppSettings(appId, lang) { 22 | return this.appRepo.getPreviewAppSettings(appId, lang); 23 | } 24 | 25 | // プレビュー環境のフォームフィールド情報を取得 26 | async getPreviewFormFields(appId, lang) { 27 | return this.appRepo.getPreviewFormFields(appId, lang); 28 | } 29 | 30 | // プレビュー環境のフォームレイアウト情報を取得 31 | async getPreviewFormLayout(appId) { 32 | return this.appRepo.getPreviewFormLayout(appId); 33 | } 34 | 35 | // レコード関連 36 | async getRecord(appId, recordId) { 37 | return this.recordRepo.getRecord(appId, recordId); 38 | } 39 | 40 | async searchRecords(appId, query, fields = []) { 41 | return this.recordRepo.searchRecords(appId, query, fields); 42 | } 43 | 44 | async createRecord(appId, fields) { 45 | return this.recordRepo.createRecord(appId, fields); 46 | } 47 | 48 | async updateRecord(record) { 49 | return this.recordRepo.updateRecord(record); 50 | } 51 | 52 | async addRecordComment(appId, recordId, text, mentions = []) { 53 | return this.recordRepo.addRecordComment(appId, recordId, text, mentions); 54 | } 55 | 56 | async getRecordAcl(appId, recordId) { 57 | return this.appRepo.getRecordAcl(appId, recordId); 58 | } 59 | 60 | async evaluateRecordsAcl(appId, recordIds) { 61 | return this.appRepo.evaluateRecordsAcl(appId, recordIds); 62 | } 63 | 64 | async updateRecordStatus(appId, recordId, action, assignee = null) { 65 | return this.recordRepo.updateRecordStatus(appId, recordId, action, assignee); 66 | } 67 | 68 | async updateRecordAssignees(appId, recordId, assignees) { 69 | return this.recordRepo.updateRecordAssignees(appId, recordId, assignees); 70 | } 71 | 72 | async getRecordComments(appId, recordId, order = 'desc', offset = 0, limit = 10) { 73 | return this.recordRepo.getRecordComments(appId, recordId, order, offset, limit); 74 | } 75 | 76 | async updateRecordComment(appId, recordId, commentId, text, mentions = []) { 77 | return this.recordRepo.updateRecordComment(appId, recordId, commentId, text, mentions); 78 | } 79 | 80 | async createRecords(appId, records) { 81 | return this.recordRepo.createRecords(appId, records); 82 | } 83 | 84 | async updateRecords(appId, records) { 85 | return this.recordRepo.updateRecords(appId, records); 86 | } 87 | 88 | async upsertRecord(appId, updateKey, fields) { 89 | return this.recordRepo.upsertRecord(appId, updateKey, fields); 90 | } 91 | 92 | async upsertRecords(appId, records) { 93 | return this.recordRepo.upsertRecords(appId, records); 94 | } 95 | 96 | async updateRecordByKey(appId, keyField, keyValue, fields) { 97 | return this.recordRepo.updateRecordByKey(appId, keyField, keyValue, fields); 98 | } 99 | 100 | // アプリ関連 101 | async getAppsInfo(params) { 102 | return this.appRepo.getAppsInfo(params); 103 | } 104 | 105 | async createApp(name, space = null, thread = null) { 106 | return this.appRepo.createApp(name, space, thread); 107 | } 108 | 109 | async addFields(appId, properties) { 110 | return this.appRepo.addFields(appId, properties); 111 | } 112 | 113 | async deployApp(apps) { 114 | return this.appRepo.deployApp(apps); 115 | } 116 | 117 | async getDeployStatus(apps) { 118 | return this.appRepo.getDeployStatus(apps); 119 | } 120 | 121 | async updateAppSettings(appId, settings) { 122 | return this.appRepo.updateAppSettings(appId, settings); 123 | } 124 | 125 | async getFormLayout(appId) { 126 | return this.appRepo.getFormLayout(appId); 127 | } 128 | 129 | async getFormFields(appId) { 130 | return this.appRepo.getFormFields(appId); 131 | } 132 | 133 | async updateFormLayout(appId, layout, revision = -1) { 134 | return this.appRepo.updateFormLayout(appId, layout, revision); 135 | } 136 | 137 | async updateFormFields(appId, properties, revision = -1) { 138 | return this.appRepo.updateFormFields(appId, properties, revision); 139 | } 140 | 141 | async deleteFormFields(appId, fields, revision = -1) { 142 | return this.appRepo.deleteFormFields(appId, fields, revision); 143 | } 144 | 145 | // ファイル関連 146 | async uploadFile(fileName, fileData) { 147 | return this.fileRepo.uploadFile(fileName, fileData); 148 | } 149 | 150 | async downloadFile(fileKey) { 151 | return this.fileRepo.downloadFile(fileKey); 152 | } 153 | 154 | // スペース関連 155 | async getSpace(spaceId) { 156 | return this.spaceRepo.getSpace(spaceId); 157 | } 158 | 159 | async updateSpace(spaceId, settings) { 160 | return this.spaceRepo.updateSpace(spaceId, settings); 161 | } 162 | 163 | async updateSpaceBody(spaceId, body) { 164 | return this.spaceRepo.updateSpaceBody(spaceId, body); 165 | } 166 | 167 | async getSpaceMembers(spaceId) { 168 | return this.spaceRepo.getSpaceMembers(spaceId); 169 | } 170 | 171 | async updateSpaceMembers(spaceId, members) { 172 | return this.spaceRepo.updateSpaceMembers(spaceId, members); 173 | } 174 | 175 | async addThread(spaceId, name) { 176 | return this.spaceRepo.addThread(spaceId, name); 177 | } 178 | 179 | async updateThread(threadId, params) { 180 | return this.spaceRepo.updateThread(threadId, params); 181 | } 182 | 183 | async addThreadComment(spaceId, threadId, comment) { 184 | return this.spaceRepo.addThreadComment(spaceId, threadId, comment); 185 | } 186 | 187 | async updateSpaceGuests(spaceId, guests) { 188 | return this.spaceRepo.updateSpaceGuests(spaceId, guests); 189 | } 190 | 191 | // アプリをスペースに移動させる 192 | async moveAppToSpace(appId, spaceId) { 193 | return this.appRepo.moveAppToSpace(appId, spaceId); 194 | } 195 | 196 | // アプリをスペースに所属させないようにする 197 | async moveAppFromSpace(appId) { 198 | return this.appRepo.moveAppFromSpace(appId); 199 | } 200 | 201 | // ユーザー関連 202 | async addGuests(guests) { 203 | return this.userRepo.addGuests(guests); 204 | } 205 | 206 | async getUsers(codes = []) { 207 | return this.userRepo.getUsers(codes); 208 | } 209 | 210 | async getGroups(codes = []) { 211 | return this.userRepo.getGroups(codes); 212 | } 213 | 214 | async getGroupUsers(groupCode) { 215 | return this.userRepo.getGroupUsers(groupCode); 216 | } 217 | 218 | // アプリのアクション設定を取得 219 | async getAppActions(appId, lang) { 220 | return this.appRepo.getAppActions(appId, lang); 221 | } 222 | 223 | // アプリのプラグイン一覧を取得 224 | async getAppPlugins(appId) { 225 | return this.appRepo.getAppPlugins(appId); 226 | } 227 | 228 | // アプリのプロセス管理設定を取得 229 | async getProcessManagement(appId, preview = false) { 230 | if (preview) { 231 | return this.appRepo.previewRepository.getPreviewProcessManagement(appId); 232 | } else { 233 | return this.appRepo.getProcessManagement(appId); 234 | } 235 | } 236 | 237 | // アプリのプロセス管理設定を更新 238 | async updateProcessManagement(appId, enable, states, actions, revision = -1) { 239 | return this.appRepo.updateProcessManagement(appId, enable, states, actions, revision); 240 | } 241 | 242 | // アプリのビュー設定を取得 243 | async getViews(appId, preview = false) { 244 | return this.appRepo.getViews(appId, preview); 245 | } 246 | 247 | // アプリのビュー設定を更新 248 | async updateViews(appId, views, revision = -1) { 249 | return this.appRepo.updateViews(appId, views, revision); 250 | } 251 | 252 | // アプリのアクセス権限を取得 253 | async getAppAcl(appId, preview = false) { 254 | return this.appRepo.getAppAcl(appId, preview); 255 | } 256 | 257 | // フィールドのアクセス権限を取得 258 | async getFieldAcl(appId, preview = false) { 259 | return this.appRepo.getFieldAcl(appId, preview); 260 | } 261 | 262 | // フィールドのアクセス権限を更新 263 | async updateFieldAcl(appId, rights, revision = -1) { 264 | return this.appRepo.updateFieldAcl(appId, rights, revision); 265 | } 266 | 267 | // グラフ設定を取得 268 | async getReports(appId, preview = false) { 269 | return this.appRepo.getReports(appId, preview); 270 | } 271 | 272 | // グラフ設定を更新 273 | async updateReports(appId, reports, revision = -1) { 274 | return this.appRepo.updateReports(appId, reports, revision); 275 | } 276 | 277 | // 通知条件設定を取得 278 | async getNotifications(appId, preview = false) { 279 | return this.appRepo.getNotifications(appId, preview); 280 | } 281 | 282 | // 通知条件設定を更新 283 | async updateNotifications(appId, notifications, revision = -1) { 284 | return this.appRepo.updateNotifications(appId, notifications, revision); 285 | } 286 | 287 | // レコード単位の通知設定を取得 288 | async getPerRecordNotifications(appId, preview = false) { 289 | return this.appRepo.getPerRecordNotifications(appId, preview); 290 | } 291 | 292 | // レコード単位の通知設定を更新 293 | async updatePerRecordNotifications(appId, notifications, revision = -1) { 294 | return this.appRepo.updatePerRecordNotifications(appId, notifications, revision); 295 | } 296 | 297 | // リマインダー通知設定を取得 298 | async getReminderNotifications(appId, preview = false) { 299 | return this.appRepo.getReminderNotifications(appId, preview); 300 | } 301 | 302 | // リマインダー通知設定を更新 303 | async updateReminderNotifications(appId, notifications, revision = -1) { 304 | return this.appRepo.updateReminderNotifications(appId, notifications, revision); 305 | } 306 | 307 | // アプリアクション設定を更新 308 | async updateAppActions(appId, actions, revision = -1) { 309 | return this.appRepo.updateAppActions(appId, actions, revision); 310 | } 311 | 312 | // プラグイン設定を更新 313 | async updatePlugins(appId, plugins, revision = -1) { 314 | return this.appRepo.updatePlugins(appId, plugins, revision); 315 | } 316 | 317 | // JavaScript/CSSカスタマイズ設定を取得 318 | async getAppCustomize(appId, preview = false) { 319 | return this.appRepo.getAppCustomize(appId, preview); 320 | } 321 | 322 | // JavaScript/CSSカスタマイズ設定を更新 323 | async updateAppCustomize(appId, scope, desktop, mobile, revision = -1) { 324 | return this.appRepo.updateAppCustomize(appId, scope, desktop, mobile, revision); 325 | } 326 | 327 | // アプリのアクセス権限を更新 328 | async updateAppAcl(appId, rights, revision = -1) { 329 | return this.appRepo.updateAppAcl(appId, rights, revision); 330 | } 331 | } 332 | ``` -------------------------------------------------------------------------------- /src/repositories/base/http/createKintoneClient.js: -------------------------------------------------------------------------------- ```javascript 1 | // src/repositories/base/http/createKintoneClient.js 2 | import { KintoneHttpClient } from './KintoneHttpClient.js'; 3 | 4 | function removePreview(params = {}) { 5 | const { preview, ...rest } = params; 6 | return { preview: preview === true, params: rest }; 7 | } 8 | 9 | export function createKintoneClient(credentials) { 10 | const http = new KintoneHttpClient(credentials); 11 | 12 | const space = { 13 | getSpace: (params) => http.get('space', params), 14 | updateSpace: (params) => http.put('space', params), 15 | updateSpaceBody: (params) => http.put('space/body', params), 16 | getSpaceMembers: (params) => http.get('space/members', params), 17 | updateSpaceMembers: (params) => http.put('space/members', params), 18 | addThread: (params) => http.post('space/thread', params), 19 | updateThread: (params) => http.put('space/thread', params), 20 | addThreadComment: (params) => http.post('space/thread/comment', params), 21 | updateSpaceGuests: (params) => http.put('space/guests', params), 22 | addGuests: (params) => http.post('guests', params) 23 | }; 24 | 25 | const app = { 26 | getApps: (params) => http.get('apps', params), 27 | addApp: async (params) => { 28 | const payload = { name: params.name }; 29 | if (params.space) { 30 | payload.space = params.space; 31 | if (params.thread) { 32 | payload.thread = params.thread; 33 | } else { 34 | const spaceInfo = await space.getSpace({ id: params.space }); 35 | if (spaceInfo && spaceInfo.defaultThread) { 36 | payload.thread = spaceInfo.defaultThread; 37 | } 38 | } 39 | } else if (params.thread) { 40 | payload.thread = params.thread; 41 | } 42 | return http.post('app', payload, { preview: true }); 43 | }, 44 | deployApp: (params) => http.post('app/deploy', params, { preview: true }), 45 | getDeployStatus: (params) => http.get('app/deploy', params, { preview: true }), 46 | getAppSettings: (params = {}) => { 47 | const { preview, params: rest } = removePreview(params); 48 | return http.get('app/settings', rest, { preview }); 49 | }, 50 | updateAppSettings: (params) => http.put('app/settings', params, { preview: true }), 51 | getProcessManagement: (params = {}) => { 52 | const { preview, params: rest } = removePreview(params); 53 | return http.get('app/status', rest, { preview }); 54 | }, 55 | updateProcessManagement: (params) => http.put('app/status', params, { preview: true }), 56 | getFormFields: (params = {}) => { 57 | const { preview, params: rest } = removePreview(params); 58 | return http.get('app/form/fields', rest, { preview }); 59 | }, 60 | addFormFields: (params) => http.post('app/form/fields', params, { preview: true }), 61 | updateFormFields: (params) => http.put('app/form/fields', params, { preview: true }), 62 | deleteFormFields: (params) => http.delete('app/form/fields', params, { preview: true }), 63 | getFormLayout: (params = {}) => { 64 | const { preview, params: rest } = removePreview(params); 65 | return http.get('app/form/layout', rest, { preview }); 66 | }, 67 | updateFormLayout: (params) => http.put('app/form/layout', params, { preview: true }), 68 | getViews: (params = {}) => { 69 | const { preview, params: rest } = removePreview(params); 70 | return http.get('app/views', rest, { preview }); 71 | }, 72 | updateViews: (params) => http.put('app/views', params, { preview: true }), 73 | getAppAcl: (params = {}) => { 74 | const { preview, params: rest } = removePreview(params); 75 | return http.get('app/acl', rest, { preview }); 76 | }, 77 | updateAppAcl: (params) => http.put('app/acl', params, { preview: true }), 78 | getFieldAcl: (params = {}) => { 79 | const { preview, params: rest } = removePreview(params); 80 | return http.get('field/acl', rest, { preview }); 81 | }, 82 | updateFieldAcl: (params) => http.put('field/acl', params, { preview: true }), 83 | getAppActions: (params = {}) => { 84 | const { preview, params: rest } = removePreview(params); 85 | return http.get('app/actions', rest, { preview }); 86 | }, 87 | updateAppActions: (params) => http.put('app/actions', params, { preview: true }), 88 | getPlugins: (params = {}) => { 89 | const { preview, params: rest } = removePreview(params); 90 | return http.get('app/plugins', rest, { preview }); 91 | }, 92 | updatePlugins: (params) => http.put('app/plugins', params, { preview: true }), 93 | getReports: (params = {}) => { 94 | const { preview, params: rest } = removePreview(params); 95 | return http.get('app/reports', rest, { preview }); 96 | }, 97 | updateReports: (params) => http.put('app/reports', params, { preview: true }), 98 | getGeneralNotifications: (params = {}) => { 99 | const { preview, params: rest } = removePreview(params); 100 | return http.get('app/notifications/general', rest, { preview }); 101 | }, 102 | updateGeneralNotifications: (params) => http.put('app/notifications/general', params, { preview: true }), 103 | getPerRecordNotifications: (params = {}) => { 104 | const { preview, params: rest } = removePreview(params); 105 | return http.get('app/notifications/perRecord', rest, { preview }); 106 | }, 107 | updatePerRecordNotifications: (params) => http.put('app/notifications/perRecord', params, { preview: true }), 108 | getReminderNotifications: (params = {}) => { 109 | const { preview, params: rest } = removePreview(params); 110 | return http.get('app/notifications/reminder', rest, { preview }); 111 | }, 112 | updateReminderNotifications: (params) => http.put('app/notifications/reminder', params, { preview: true }), 113 | getAppCustomize: (params = {}) => { 114 | const { preview, params: rest } = removePreview(params); 115 | return http.get('app/customize', rest, { preview }); 116 | }, 117 | updateAppCustomize: (params) => http.put('app/customize', params, { preview: true }), 118 | getRecordAcl: (params = {}) => { 119 | const { preview, params: rest } = removePreview(params); 120 | return http.get('record/acl', rest, { preview }); 121 | }, 122 | evaluateRecordsAcl: (params) => http.get('records/acl/evaluate', params), 123 | move: (params) => http.post('app/move', params) 124 | }; 125 | 126 | const record = { 127 | getRecord: (params) => http.get('record', params), 128 | addRecord: (params) => http.post('record', params), 129 | updateRecord: (params) => http.put('record', params), 130 | updateRecordByUpdateKey: (params) => http.put('record', params), 131 | getRecords: (params) => http.get('records', params), 132 | addRecords: (params) => http.post('records', params), 133 | updateRecords: (params) => http.put('records', params), 134 | upsertRecord: async (params) => { 135 | const { app: appId, updateKey, record: recordData } = params; 136 | const response = await http.put('records', { 137 | app: appId, 138 | upsert: true, 139 | records: [ 140 | { 141 | updateKey, 142 | record: recordData 143 | } 144 | ] 145 | }); 146 | 147 | if (!response || !Array.isArray(response.records) || response.records.length === 0) { 148 | throw new Error('Unexpected response format from records upsert operation.'); 149 | } 150 | 151 | const result = response.records[0]; 152 | return { 153 | id: result.id, 154 | revision: result.revision, 155 | operation: result.operation 156 | }; 157 | }, 158 | upsertRecords: async (params) => { 159 | const { app: appId, records } = params; 160 | if (!Array.isArray(records) || records.length === 0) { 161 | throw new Error('records must be a non-empty array when calling upsertRecords.'); 162 | } 163 | 164 | const formattedRecords = records.map((entry) => ({ 165 | updateKey: entry.updateKey, 166 | record: entry.record 167 | })); 168 | 169 | const response = await http.put('records', { 170 | app: appId, 171 | upsert: true, 172 | records: formattedRecords 173 | }); 174 | 175 | if (!response || !Array.isArray(response.records) || response.records.length === 0) { 176 | throw new Error('Unexpected response format from records upsert operation.'); 177 | } 178 | 179 | return response.records.map((recordResult) => ({ 180 | id: recordResult.id, 181 | revision: recordResult.revision, 182 | operation: recordResult.operation 183 | })); 184 | }, 185 | addRecordComment: (params) => http.post('record/comment', params), 186 | getRecordComments: (params) => http.get('record/comments', params), 187 | updateRecordComment: (params) => http.put('record/comment', params), 188 | updateRecordStatus: (params) => http.put('record/status', params), 189 | updateRecordAssignees: (params) => http.put('record/assignees', params) 190 | }; 191 | 192 | const file = { 193 | uploadFile: async (params) => { 194 | const form = new FormData(); 195 | const fileData = params.file?.data; 196 | const fileName = params.file?.name; 197 | 198 | if (fileData === undefined || fileData === null) { 199 | throw new Error('Missing file data for upload.'); 200 | } 201 | 202 | const blob = fileData instanceof Blob ? fileData : new Blob([fileData]); 203 | form.append('file', blob, fileName ?? 'upload.bin'); 204 | return http.post('file', form); 205 | }, 206 | downloadFile: async (params) => { 207 | const response = await http.get('file', params, { 208 | responseType: 'arraybuffer', 209 | rawResponse: true 210 | }); 211 | const contentType = response.headers['content-type'] || response.headers['Content-Type']; 212 | return { 213 | data: response.data, 214 | contentType 215 | }; 216 | } 217 | }; 218 | 219 | return { app, record, space, file }; 220 | } 221 | ```