@jinshuju/mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,11 +49,11 @@ To check it works, ask the assistant to "list my Jinshuju forms".
49
49
 
50
50
  ## Tools
51
51
 
52
- 60 tools with the same names as those documented for the [Jinshuju MCP Server](https://open.jinshuju.net/mcp). Each tool maps to one operation in openapi.yaml; a few combine two operations:
52
+ The 60 tools documented for the [Jinshuju MCP Server](https://open.jinshuju.net/mcp), with the same names, plus delete_form. Each tool maps to one operation in openapi.yaml; a few combine two operations:
53
53
 
54
54
  | Category | Tools |
55
55
  | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
- | Forms | list_forms, get_form, create_form, edit_form, copy_form, move_form, list_form_cooperators, get_field_rules, edit_field_rules, check_field_data, preview_field_type_change, edit_theme |
56
+ | Forms | list_forms, get_form, create_form, edit_form, copy_form, delete_form, move_form, list_form_cooperators, get_field_rules, edit_field_rules, check_field_data, preview_field_type_change, edit_theme |
57
57
  | Exams / evaluations | create_exam_form, edit_exam_form, create_evaluation_form, edit_evaluation_form (create / edit the form + its scene settings, two requests) |
58
58
  | Entries | list_entries, get_entry, create_entry, update_entry, delete_entry, create_entries, patch_entries, count_entries, aggregate_entries, get_form_data_summary, search_entries_in_forms, list_form_entry_stats, import_entries_from_file |
59
59
  | What I submitted | list_my_submitted_forms, list_my_submitted_entries, search_my_submitted_entries |
@@ -12,7 +12,8 @@
12
12
  * - intent: every tool gets a context argument where the model says what the user wants to do ($mcp_intent). The SDK
13
13
  * scrubs emails / phone numbers / IPs / card numbers from it before sending; JINSHUJU_MCP_COLLECT_INTENT=false turns
14
14
  * it off entirely (the argument is no longer offered and nothing is reported);
15
- * - distinct_id = the Jinshuju user id (from GET /me); $session_id is derived from the token hash: one session per token;
15
+ * - distinct_id = the Jinshuju user id (from GET /me, or the token info when the token lacks the users scope; see
16
+ * auth.ts); $session_id is derived from the token hash: one session per token;
16
17
  * - a session's initialize is reported at most once a day (deduplicated in process; best effort across Workers instances).
17
18
  *
18
19
  * Every other SDK injection stays off: no llm_model / conversation_id arguments, no get_more_tools / send_feedback tools.
package/dist/app.js CHANGED
@@ -35,7 +35,12 @@ export function createApp(options = {}) {
35
35
  const cfg = { ...DEFAULTS, ...options.config };
36
36
  const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
37
37
  const api = new JinshujuApi({ baseUrl: cfg.apiBaseUrl, fetch: fetchImpl, timeoutMs: cfg.timeoutMs });
38
- const verifier = new TokenVerifier({ api, cacheTtlMs: cfg.tokenCacheTtlMs });
38
+ const verifier = new TokenVerifier({
39
+ api,
40
+ cacheTtlMs: cfg.tokenCacheTtlMs,
41
+ authServer: cfg.authServer,
42
+ fetch: fetchImpl
43
+ });
39
44
  const analytics = options.analytics === undefined ? analyticsFromConfig(cfg, fetchImpl) : (options.analytics ?? undefined);
40
45
  const instructions = analytics?.collectIntent ? `${INSTRUCTIONS}\n${CONTEXT_INSTRUCTIONS}` : INSTRUCTIONS;
41
46
  let metadata;
package/dist/auth.d.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  * - the request's Bearer (an OAuth token or a personal / account access token) goes to API v1 as is;
7
7
  * - the API decides whether a token is valid: one light call (GET /me) checks it, and the result is cached by token
8
8
  * hash for a while, so every tool call does not spend an extra API request;
9
+ * - GET /me also names the user. An OAuth token without the `users` scope gets 403 there, so the user id then comes from
10
+ * the authorization server's token info, which answers whatever the token's scopes;
9
11
  * - with no token or an invalid one, answers 401 + WWW-Authenticate: Bearer resource_metadata=..., and the client
10
12
  * goes through OAuth from there.
11
13
  */
@@ -14,6 +16,10 @@ import type { JinshujuApi } from './api.js';
14
16
  export interface VerifierOptions {
15
17
  api: JinshujuApi;
16
18
  cacheTtlMs: number;
19
+ /** OAuth authorization server (issuer). When set, a valid token GET /me does not name is looked up at its token info. */
20
+ authServer?: string;
21
+ /** The fetch used for the token info lookup; the global fetch by default */
22
+ fetch?: typeof fetch;
17
23
  maxEntries?: number;
18
24
  now?: () => number;
19
25
  }
@@ -23,12 +29,19 @@ export declare class TokenVerifier {
23
29
  private readonly options;
24
30
  private readonly cache;
25
31
  private readonly now;
32
+ private readonly fetchImpl;
26
33
  constructor(options: VerifierOptions);
27
34
  /** Returns AuthInfo when valid, undefined when not. A network failure counts as invalid but is not cached. */
28
35
  verify(token: string): Promise<AuthInfo | undefined>;
29
36
  forget(token: string): Promise<void>;
37
+ /**
38
+ * The user behind an OAuth token, from the authorization server's token info (Doorkeeper's GET /oauth/token/info):
39
+ * it answers for any scope, and its resource_owner_id is the id GET /me returns. It gives no name. Personal and
40
+ * account tokens are not OAuth tokens and get 401 there. Best effort: on any failure the user stays unnamed.
41
+ */
42
+ private tokenOwner;
30
43
  }
31
- /** The Jinshuju user id in AuthInfo (the id from GET /me), undefined when no user was identified; analytics and Sentry both identify the user by it. */
44
+ /** The Jinshuju user id in AuthInfo (from GET /me or the token info), undefined when no user was identified; analytics and Sentry both identify the user by it. */
32
45
  export declare function userIdOf(authInfo: AuthInfo | undefined): string | undefined;
33
46
  export declare function bearerToken(request: Request): string | undefined;
34
47
  export declare function challenge(resourceMetadataUrl: string, message: string): Response;
package/dist/auth.js CHANGED
@@ -6,10 +6,14 @@
6
6
  * - the request's Bearer (an OAuth token or a personal / account access token) goes to API v1 as is;
7
7
  * - the API decides whether a token is valid: one light call (GET /me) checks it, and the result is cached by token
8
8
  * hash for a while, so every tool call does not spend an extra API request;
9
+ * - GET /me also names the user. An OAuth token without the `users` scope gets 403 there, so the user id then comes from
10
+ * the authorization server's token info, which answers whatever the token's scopes;
9
11
  * - with no token or an invalid one, answers 401 + WWW-Authenticate: Bearer resource_metadata=..., and the client
10
12
  * goes through OAuth from there.
11
13
  */
12
14
  import { bearerAuthChallengeResponse, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
15
+ /** Time limit of the token info lookup: it only names the user, so a slow authorization server must not hold up the request for long. */
16
+ const TOKEN_INFO_TIMEOUT_MS = 5_000;
13
17
  /** Token hash: the key of the verification cache, and the source of the analytics session id. */
14
18
  export async function sha256(text) {
15
19
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
@@ -19,9 +23,12 @@ export class TokenVerifier {
19
23
  options;
20
24
  cache = new Map();
21
25
  now;
26
+ fetchImpl;
22
27
  constructor(options) {
23
28
  this.options = options;
24
29
  this.now = options.now ?? Date.now;
30
+ // Don't store the global fetch as is: called as a method, its `this` is not the global object, and workerd throws Illegal invocation.
31
+ this.fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
25
32
  }
26
33
  /** Returns AuthInfo when valid, undefined when not. A network failure counts as invalid but is not cached. */
27
34
  async verify(token) {
@@ -39,7 +46,7 @@ export class TokenVerifier {
39
46
  token,
40
47
  clientId: 'jinshuju',
41
48
  scopes: [],
42
- extra: me ? { userId: me.id, userName: me.name } : undefined
49
+ extra: me ? { userId: me.id, userName: me.name } : await this.tokenOwner(token)
43
50
  };
44
51
  if (this.cache.size >= (this.options.maxEntries ?? 2000)) {
45
52
  const oldest = this.cache.keys().next().value;
@@ -52,8 +59,31 @@ export class TokenVerifier {
52
59
  async forget(token) {
53
60
  this.cache.delete(await sha256(token));
54
61
  }
62
+ /**
63
+ * The user behind an OAuth token, from the authorization server's token info (Doorkeeper's GET /oauth/token/info):
64
+ * it answers for any scope, and its resource_owner_id is the id GET /me returns. It gives no name. Personal and
65
+ * account tokens are not OAuth tokens and get 401 there. Best effort: on any failure the user stays unnamed.
66
+ */
67
+ async tokenOwner(token) {
68
+ if (!this.options.authServer)
69
+ return undefined;
70
+ try {
71
+ const res = await this.fetchImpl(`${this.options.authServer.replace(/\/$/, '')}/oauth/token/info`, {
72
+ headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
73
+ signal: AbortSignal.timeout(TOKEN_INFO_TIMEOUT_MS)
74
+ });
75
+ if (res.status !== 200)
76
+ return undefined;
77
+ const owner = (await res.json()).resource_owner_id;
78
+ return owner === undefined || owner === null || owner === '' ? undefined : { userId: owner };
79
+ }
80
+ catch (error) {
81
+ console.warn('token info lookup failed:', error);
82
+ return undefined;
83
+ }
84
+ }
55
85
  }
56
- /** The Jinshuju user id in AuthInfo (the id from GET /me), undefined when no user was identified; analytics and Sentry both identify the user by it. */
86
+ /** The Jinshuju user id in AuthInfo (from GET /me or the token info), undefined when no user was identified; analytics and Sentry both identify the user by it. */
57
87
  export function userIdOf(authInfo) {
58
88
  const id = authInfo?.extra?.userId;
59
89
  return id === undefined || id === null || id === '' ? undefined : String(id);
@@ -2,17 +2,18 @@
2
2
  "generatedFrom": {
3
3
  "title": "Jinshuju API v1",
4
4
  "version": "1.0",
5
- "sha256": "288548a94e3932e7"
5
+ "sha256": "ecaf2b639eaf7555"
6
6
  },
7
7
  "baseUrl": "https://jinshuju.net/api/v1",
8
- "toolCount": 60,
9
- "operationCount": 128,
8
+ "toolCount": 61,
9
+ "operationCount": 129,
10
10
  "tools": {
11
11
  "list_forms": "listForms",
12
12
  "get_form": "getForm",
13
13
  "create_form": "createForm",
14
14
  "edit_form": "updateForm",
15
15
  "copy_form": "copyForm",
16
+ "delete_form": "deleteForm",
16
17
  "move_form": "moveFormToFolder",
17
18
  "list_form_cooperators": "listFormCooperators",
18
19
  "get_field_rules": "listFieldRules",
@@ -190,6 +190,28 @@
190
190
  ],
191
191
  "responseSchema": "Form"
192
192
  },
193
+ "deleteForm": {
194
+ "operationId": "deleteForm",
195
+ "summary": "Delete a form",
196
+ "description": "Moves the form and its entries to the trash, from which the product can restore it; on success, returns the form token.",
197
+ "tag": "Forms",
198
+ "method": "delete",
199
+ "path": "/forms/{form_token}",
200
+ "params": [
201
+ {
202
+ "name": "form_token",
203
+ "in": "path",
204
+ "required": true
205
+ }
206
+ ],
207
+ "paramNames": [
208
+ "form_token"
209
+ ],
210
+ "successStatus": 200,
211
+ "scopes": [
212
+ "forms"
213
+ ]
214
+ },
193
215
  "checkFormFieldData": {
194
216
  "operationId": "checkFormFieldData",
195
217
  "summary": "Check whether fields already hold data",
@@ -266,7 +288,8 @@
266
288
  {
267
289
  "name": "fields",
268
290
  "in": "query",
269
- "required": false
291
+ "required": false,
292
+ "array": true
270
293
  },
271
294
  {
272
295
  "name": "filters",
@@ -466,7 +489,8 @@
466
489
  {
467
490
  "name": "fields",
468
491
  "in": "query",
469
- "required": false
492
+ "required": false,
493
+ "array": true
470
494
  },
471
495
  {
472
496
  "name": "include_overview",
@@ -623,7 +647,8 @@
623
647
  {
624
648
  "name": "fields",
625
649
  "in": "query",
626
- "required": false
650
+ "required": false,
651
+ "array": true
627
652
  },
628
653
  {
629
654
  "name": "include_overview",
@@ -733,7 +758,8 @@
733
758
  {
734
759
  "name": "fields",
735
760
  "in": "query",
736
- "required": false
761
+ "required": false,
762
+ "array": true
737
763
  },
738
764
  {
739
765
  "name": "include_labels",
@@ -2033,7 +2059,8 @@
2033
2059
  {
2034
2060
  "name": "fields",
2035
2061
  "in": "query",
2036
- "required": false
2062
+ "required": false,
2063
+ "array": true
2037
2064
  },
2038
2065
  {
2039
2066
  "name": "filters",
@@ -2233,7 +2260,8 @@
2233
2260
  {
2234
2261
  "name": "fields",
2235
2262
  "in": "query",
2236
- "required": false
2263
+ "required": false,
2264
+ "array": true
2237
2265
  },
2238
2266
  {
2239
2267
  "name": "include_overview",
@@ -2390,7 +2418,8 @@
2390
2418
  {
2391
2419
  "name": "fields",
2392
2420
  "in": "query",
2393
- "required": false
2421
+ "required": false,
2422
+ "array": true
2394
2423
  },
2395
2424
  {
2396
2425
  "name": "include_overview",
@@ -2444,7 +2473,8 @@
2444
2473
  {
2445
2474
  "name": "fields",
2446
2475
  "in": "query",
2447
- "required": false
2476
+ "required": false,
2477
+ "array": true
2448
2478
  },
2449
2479
  {
2450
2480
  "name": "include_labels",
@@ -2948,7 +2978,7 @@
2948
2978
  "createCheckinCampaign": {
2949
2979
  "operationId": "createCheckinCampaign",
2950
2980
  "summary": "Create a check-in campaign",
2951
- "description": "Turns check-in on for a form. The caller must be able to reach the form.\n- Same limits as picking a form in the web app: returns 400 when the form has WeChat red envelope, lucky wheel, certificate or confirmation code on, is bound to a plugin that takes over the after-submit redirect (a check-in campaign with a check-in code included), or its after-submit action is not the default message. Turn that off first.\n- Only one check-in campaign is in effect on a form at a time; creating one turns off the one that was in effect.\n- Without `checkin_field_api_code`, the form's first mobile field is used, then its first name field; with neither, returns 400. The check-in field cannot be changed afterwards.\n- The campaign belongs to the caller, or to the account owner with an account-level credential. The caller is also added as a verifier.\n- `enable_checkin_code` (on by default) rewrites the form's after-submit redirect and data push URL to the check-in code page, and overrides any Webhook already on the form (and the other way round).",
2981
+ "description": "Turns check-in on for a form. The caller must be able to reach the form.\n- Same limits as picking a form in the web app: returns 400 when the form has WeChat red envelope, lucky wheel, certificate or confirmation code on, already has a check-in campaign with a check-in code in effect, is bound to another plugin that takes over the after-submit redirect, or its after-submit action is not the default message. Turn that off first.\n- Only one check-in campaign is in effect on a form at a time; creating one turns off the one that was in effect.\n- Without `checkin_field_api_code`, the form's first mobile field is used, then its first name field; with neither, returns 400. The check-in field cannot be changed afterwards.\n- The campaign belongs to the caller, or to the account owner with an account-level credential. The caller is also added as a verifier.\n- `enable_checkin_code` (on by default) rewrites the form's after-submit redirect and data push URL to the check-in code page, and overrides any Webhook already on the form (and the other way round).",
2952
2982
  "tag": "Plugins",
2953
2983
  "method": "post",
2954
2984
  "path": "/plugins/checkin/campaigns",
@@ -3033,7 +3063,7 @@
3033
3063
  "paramNames": [
3034
3064
  "id"
3035
3065
  ],
3036
- "successStatus": 204,
3066
+ "successStatus": 200,
3037
3067
  "scopes": [
3038
3068
  "form_setting"
3039
3069
  ]
@@ -3787,7 +3817,8 @@
3787
3817
  {
3788
3818
  "name": "form_tokens",
3789
3819
  "in": "query",
3790
- "required": false
3820
+ "required": false,
3821
+ "array": true
3791
3822
  },
3792
3823
  {
3793
3824
  "name": "filters",
@@ -3822,7 +3853,8 @@
3822
3853
  {
3823
3854
  "name": "form_tokens",
3824
3855
  "in": "query",
3825
- "required": false
3856
+ "required": false,
3857
+ "array": true
3826
3858
  },
3827
3859
  {
3828
3860
  "name": "filters",
@@ -3940,7 +3972,8 @@
3940
3972
  {
3941
3973
  "name": "fields",
3942
3974
  "in": "query",
3943
- "required": false
3975
+ "required": false,
3976
+ "array": true
3944
3977
  },
3945
3978
  {
3946
3979
  "name": "filters",
@@ -4006,7 +4039,8 @@
4006
4039
  {
4007
4040
  "name": "form_tokens",
4008
4041
  "in": "query",
4009
- "required": false
4042
+ "required": false,
4043
+ "array": true
4010
4044
  }
4011
4045
  ],
4012
4046
  "paramNames": [
@@ -523,7 +523,7 @@
523
523
  },
524
524
  "expired": {
525
525
  "type": "boolean",
526
- "description": "Whether the organization plan has expired"
526
+ "description": "Whether the current paid plan has expired; false for accounts that have never paid"
527
527
  }
528
528
  }
529
529
  },
@@ -1828,7 +1828,7 @@
1828
1828
  },
1829
1829
  "checkin_field_api_code": {
1830
1830
  "type": "string",
1831
- "description": "Check-in field; defaults to the first mobile field, then the first name field. Cannot be changed afterwards"
1831
+ "description": "Check-in field, a mobile, name, email, ID card or single-line text field; defaults to the first mobile field, then the first name field. Cannot be changed afterwards"
1832
1832
  },
1833
1833
  "name": {
1834
1834
  "type": "string",
@@ -1840,7 +1840,7 @@
1840
1840
  "string",
1841
1841
  "null"
1842
1842
  ],
1843
- "description": "Field shown when verifying, not the check-in field; null clears it"
1843
+ "description": "Field shown when verifying, not the check-in field; a mobile, name, email, ID card or single-line text field. null clears it"
1844
1844
  },
1845
1845
  "enable_checkin_code": {
1846
1846
  "type": "boolean",
@@ -1977,7 +1977,7 @@
1977
1977
  "string",
1978
1978
  "null"
1979
1979
  ],
1980
- "description": "Field shown when verifying, not the check-in field; null clears it"
1980
+ "description": "Field shown when verifying, not the check-in field; a mobile, name, email, ID card or single-line text field. null clears it"
1981
1981
  },
1982
1982
  "enable_checkin_code": {
1983
1983
  "type": "boolean",
@@ -2482,6 +2482,11 @@
2482
2482
  "folder_token": {
2483
2483
  "type": "string",
2484
2484
  "description": "Target folder token or id; when omitted, the copy is not put in a folder"
2485
+ },
2486
+ "with_notification_rules": {
2487
+ "type": "boolean",
2488
+ "default": false,
2489
+ "description": "Copy the notification rules too; takes manage permission on the source form, and copies none when the caller is outside the source form creator's account"
2485
2490
  }
2486
2491
  },
2487
2492
  "additionalProperties": false
@@ -25863,13 +25868,22 @@
25863
25868
  "type": "object",
25864
25869
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
25865
25870
  "required": [
25866
- "field",
25867
25871
  "operator"
25868
25872
  ],
25869
25873
  "properties": {
25870
25874
  "field": {
25871
25875
  "type": "string",
25872
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
25876
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
25877
+ },
25878
+ "api_code": {
25879
+ "type": "string",
25880
+ "deprecated": true,
25881
+ "description": "Deprecated alias of `field`; use `field`."
25882
+ },
25883
+ "trigger": {
25884
+ "type": "string",
25885
+ "deprecated": true,
25886
+ "description": "Deprecated alias of `field`; use `field`."
25873
25887
  },
25874
25888
  "operator": {
25875
25889
  "type": "string",
@@ -77,13 +77,22 @@
77
77
  "type": "object",
78
78
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
79
79
  "required": [
80
- "field",
81
80
  "operator"
82
81
  ],
83
82
  "properties": {
84
83
  "field": {
85
84
  "type": "string",
86
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
85
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
86
+ },
87
+ "api_code": {
88
+ "type": "string",
89
+ "deprecated": true,
90
+ "description": "Deprecated alias of `field`; use `field`."
91
+ },
92
+ "trigger": {
93
+ "type": "string",
94
+ "deprecated": true,
95
+ "description": "Deprecated alias of `field`; use `field`."
87
96
  },
88
97
  "operator": {
89
98
  "type": "string",
@@ -299,7 +308,7 @@
299
308
  {
300
309
  "operationId": "createForm",
301
310
  "summary": "Create a form",
302
- "description": "Create a form\nCreates a form from a list of field definitions and returns the full form structure. `name` and `fields` are required.\n- `scene` sets the form's scene; exam / evaluation question types (such as `SingleSelect`, `FillInBlank`) can only be used in the scenes that support them.\n- `layout=card` (paged) does not support PageBreak, Matrix*, TableField, GoodsField, ESignatureField, FormAssociation, MultipleBlanksField or any kind of Widget field.\n- The folder named by `folder_token` must be manageable and of kind form; otherwise returns 404 / 400 respectively.\n- `setting` accepts only whitelisted keys (see `FormSettingWriteInput`); `setting.notification_rules` replaces, as a whole, the notification rules created through the API.\n- The returned form structure carries `url` (the public fill URL).\nReturns schema \"Form\" (see get_schema).\nEach item of fields branches on type: get_schema(\"FieldInput\") lists the types, then get_schema(\"<Type>Input\") (e.g. \"TextFieldInput\") shows the properties that type accepts; do not guess field properties. For an exam or evaluation form use create_exam_form / create_evaluation_form.",
311
+ "description": "Create a form\nCreates a form from a list of field definitions and returns the full form structure. `name` and `fields` are required.\n- `scene` sets the form's scene; exam / evaluation question types (such as `SingleSelect`, `FillInBlank`) can only be used in the scenes that support them.\n- `layout=card` (paged) does not support PageBreak, Matrix*, TableField, GoodsField, ESignatureField, FormAssociation, MultipleBlanksField or any kind of Widget field.\n- The folder named by `folder_token` must be manageable and of kind form; otherwise returns 404 / 400 respectively.\n- `setting` accepts only whitelisted keys (see `FormSettingWriteInput`); `setting.notification_rules` replaces, as a whole, the notification rules created through the API.\n- The returned form structure carries `url` (the public fill URL).\nReturns schema \"Form\" (see get_schema).\nEach item of fields branches on type: get_schema(\"FieldInput\") maps each type to its schema in x-variants (TextField → \"TextFieldInput\", SingleSelect → \"SingleSelectInput\"), and get_schema on that name shows the properties that type accepts; do not guess field properties or schema names. For an exam or evaluation form use create_exam_form / create_evaluation_form.",
303
312
  "tag": "Forms",
304
313
  "method": "post",
305
314
  "path": "/forms",
@@ -795,6 +804,11 @@
795
804
  "folder_token": {
796
805
  "type": "string",
797
806
  "description": "Target folder token or id; when omitted, the copy is not put in a folder"
807
+ },
808
+ "with_notification_rules": {
809
+ "type": "boolean",
810
+ "default": false,
811
+ "description": "Copy the notification rules too; takes manage permission on the source form, and copies none when the caller is outside the source form creator's account"
798
812
  }
799
813
  },
800
814
  "additionalProperties": false,
@@ -809,6 +823,60 @@
809
823
  "openWorldHint": false
810
824
  }
811
825
  },
826
+ {
827
+ "operationId": "deleteForm",
828
+ "summary": "Delete a form",
829
+ "description": "Delete a form\nMoves the form and its entries to the trash, from which the product can restore it; on success, returns the form token.",
830
+ "tag": "Forms",
831
+ "method": "delete",
832
+ "path": "/forms/{form_token}",
833
+ "params": [
834
+ {
835
+ "name": "form_token",
836
+ "in": "path",
837
+ "required": true
838
+ }
839
+ ],
840
+ "paramNames": [
841
+ "form_token"
842
+ ],
843
+ "successStatus": 200,
844
+ "scopes": [
845
+ "forms"
846
+ ],
847
+ "name": "delete_form",
848
+ "title": "Delete a form",
849
+ "inputSchema": {
850
+ "type": "object",
851
+ "properties": {
852
+ "form_token": {
853
+ "type": "string",
854
+ "description": "Form token (`token` from `GET /forms`); a 24-character hexadecimal form id is also accepted."
855
+ }
856
+ },
857
+ "additionalProperties": false,
858
+ "required": [
859
+ "form_token"
860
+ ]
861
+ },
862
+ "outputSchema": {
863
+ "type": "object",
864
+ "required": [
865
+ "token"
866
+ ],
867
+ "properties": {
868
+ "token": {
869
+ "type": "string"
870
+ }
871
+ }
872
+ },
873
+ "annotations": {
874
+ "readOnlyHint": false,
875
+ "destructiveHint": true,
876
+ "idempotentHint": true,
877
+ "openWorldHint": false
878
+ }
879
+ },
812
880
  {
813
881
  "operationId": "moveFormToFolder",
814
882
  "summary": "Move a form into a folder",
@@ -1291,7 +1359,8 @@
1291
1359
  {
1292
1360
  "name": "fields",
1293
1361
  "in": "query",
1294
- "required": false
1362
+ "required": false,
1363
+ "array": true
1295
1364
  },
1296
1365
  {
1297
1366
  "name": "filters",
@@ -1363,7 +1432,13 @@
1363
1432
  "description": "Form token (`token` from `GET /forms`); a 24-character hexadecimal form id is also accepted."
1364
1433
  },
1365
1434
  "fields": {
1366
- "type": "string",
1435
+ "type": [
1436
+ "array",
1437
+ "string"
1438
+ ],
1439
+ "items": {
1440
+ "type": "string"
1441
+ },
1367
1442
  "description": "Return only these fields. A JSON array, a comma-separated string or a repeated parameter, of field api_codes; the serial number is always returned. Returns 400 listing the available fields when an api_code is unknown."
1368
1443
  },
1369
1444
  "filters": {
@@ -1375,13 +1450,22 @@
1375
1450
  "type": "object",
1376
1451
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
1377
1452
  "required": [
1378
- "field",
1379
1453
  "operator"
1380
1454
  ],
1381
1455
  "properties": {
1382
1456
  "field": {
1383
1457
  "type": "string",
1384
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
1458
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
1459
+ },
1460
+ "api_code": {
1461
+ "type": "string",
1462
+ "deprecated": true,
1463
+ "description": "Deprecated alias of `field`; use `field`."
1464
+ },
1465
+ "trigger": {
1466
+ "type": "string",
1467
+ "deprecated": true,
1468
+ "description": "Deprecated alias of `field`; use `field`."
1385
1469
  },
1386
1470
  "operator": {
1387
1471
  "type": "string",
@@ -1562,7 +1646,8 @@
1562
1646
  {
1563
1647
  "name": "fields",
1564
1648
  "in": "query",
1565
- "required": false
1649
+ "required": false,
1650
+ "array": true
1566
1651
  },
1567
1652
  {
1568
1653
  "name": "include_labels",
@@ -1595,7 +1680,13 @@
1595
1680
  "description": "Entry serial number"
1596
1681
  },
1597
1682
  "fields": {
1598
- "type": "string",
1683
+ "type": [
1684
+ "array",
1685
+ "string"
1686
+ ],
1687
+ "items": {
1688
+ "type": "string"
1689
+ },
1599
1690
  "description": "Return only these fields. A JSON array, a comma-separated string or a repeated parameter, of field api_codes; the serial number is always returned. Returns 400 listing the available fields when an api_code is unknown."
1600
1691
  },
1601
1692
  "include_labels": {
@@ -2250,13 +2341,22 @@
2250
2341
  "type": "object",
2251
2342
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
2252
2343
  "required": [
2253
- "field",
2254
2344
  "operator"
2255
2345
  ],
2256
2346
  "properties": {
2257
2347
  "field": {
2258
2348
  "type": "string",
2259
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
2349
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
2350
+ },
2351
+ "api_code": {
2352
+ "type": "string",
2353
+ "deprecated": true,
2354
+ "description": "Deprecated alias of `field`; use `field`."
2355
+ },
2356
+ "trigger": {
2357
+ "type": "string",
2358
+ "deprecated": true,
2359
+ "description": "Deprecated alias of `field`; use `field`."
2260
2360
  },
2261
2361
  "operator": {
2262
2362
  "type": "string",
@@ -2336,7 +2436,8 @@
2336
2436
  {
2337
2437
  "name": "fields",
2338
2438
  "in": "query",
2339
- "required": false
2439
+ "required": false,
2440
+ "array": true
2340
2441
  },
2341
2442
  {
2342
2443
  "name": "include_overview",
@@ -2377,7 +2478,13 @@
2377
2478
  "description": "Form token (`token` from `GET /forms`); a 24-character hexadecimal form id is also accepted."
2378
2479
  },
2379
2480
  "fields": {
2380
- "type": "string",
2481
+ "type": [
2482
+ "array",
2483
+ "string"
2484
+ ],
2485
+ "items": {
2486
+ "type": "string"
2487
+ },
2381
2488
  "description": "Summarize only these fields: field api_codes, comma-separated (or repeat `fields[]=`), returned in the order given. Defaults to every analyzable field. Returns 400, listing the analyzable fields, when a field is not analyzable or the caller has no permission to read it."
2382
2489
  },
2383
2490
  "include_overview": {
@@ -2394,13 +2501,22 @@
2394
2501
  "type": "object",
2395
2502
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
2396
2503
  "required": [
2397
- "field",
2398
2504
  "operator"
2399
2505
  ],
2400
2506
  "properties": {
2401
2507
  "field": {
2402
2508
  "type": "string",
2403
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
2509
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
2510
+ },
2511
+ "api_code": {
2512
+ "type": "string",
2513
+ "deprecated": true,
2514
+ "description": "Deprecated alias of `field`; use `field`."
2515
+ },
2516
+ "trigger": {
2517
+ "type": "string",
2518
+ "deprecated": true,
2519
+ "description": "Deprecated alias of `field`; use `field`."
2404
2520
  },
2405
2521
  "operator": {
2406
2522
  "type": "string",
@@ -2480,7 +2596,8 @@
2480
2596
  {
2481
2597
  "name": "form_tokens",
2482
2598
  "in": "query",
2483
- "required": false
2599
+ "required": false,
2600
+ "array": true
2484
2601
  },
2485
2602
  {
2486
2603
  "name": "filters",
@@ -2510,7 +2627,13 @@
2510
2627
  "description": "Search keyword (at most 200 characters); returns 400 when empty."
2511
2628
  },
2512
2629
  "form_tokens": {
2513
- "type": "string",
2630
+ "type": [
2631
+ "array",
2632
+ "string"
2633
+ ],
2634
+ "items": {
2635
+ "type": "string"
2636
+ },
2514
2637
  "description": "Tokens (ids also accepted) of the forms or tables to search, comma-separated (or repeat `form_tokens[]=`), at most 10 after deduplication. When omitted, every accessible form is searched."
2515
2638
  },
2516
2639
  "filters": {
@@ -2522,13 +2645,22 @@
2522
2645
  "type": "object",
2523
2646
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
2524
2647
  "required": [
2525
- "field",
2526
2648
  "operator"
2527
2649
  ],
2528
2650
  "properties": {
2529
2651
  "field": {
2530
2652
  "type": "string",
2531
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
2653
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
2654
+ },
2655
+ "api_code": {
2656
+ "type": "string",
2657
+ "deprecated": true,
2658
+ "description": "Deprecated alias of `field`; use `field`."
2659
+ },
2660
+ "trigger": {
2661
+ "type": "string",
2662
+ "deprecated": true,
2663
+ "description": "Deprecated alias of `field`; use `field`."
2532
2664
  },
2533
2665
  "operator": {
2534
2666
  "type": "string",
@@ -2761,7 +2893,8 @@
2761
2893
  {
2762
2894
  "name": "fields",
2763
2895
  "in": "query",
2764
- "required": false
2896
+ "required": false,
2897
+ "array": true
2765
2898
  },
2766
2899
  {
2767
2900
  "name": "filters",
@@ -2820,7 +2953,13 @@
2820
2953
  "description": "Form token (`token` from `GET /my/forms`); a 24-character hexadecimal form id is also accepted."
2821
2954
  },
2822
2955
  "fields": {
2823
- "type": "string",
2956
+ "type": [
2957
+ "array",
2958
+ "string"
2959
+ ],
2960
+ "items": {
2961
+ "type": "string"
2962
+ },
2824
2963
  "description": "Return only these fields. A comma-separated string or an array parameter (`fields[]=`) of field api_codes; unknown or non-public fields are skipped. The serial number is always returned."
2825
2964
  },
2826
2965
  "filters": {
@@ -2832,13 +2971,22 @@
2832
2971
  "type": "object",
2833
2972
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
2834
2973
  "required": [
2835
- "field",
2836
2974
  "operator"
2837
2975
  ],
2838
2976
  "properties": {
2839
2977
  "field": {
2840
2978
  "type": "string",
2841
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
2979
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
2980
+ },
2981
+ "api_code": {
2982
+ "type": "string",
2983
+ "deprecated": true,
2984
+ "description": "Deprecated alias of `field`; use `field`."
2985
+ },
2986
+ "trigger": {
2987
+ "type": "string",
2988
+ "deprecated": true,
2989
+ "description": "Deprecated alias of `field`; use `field`."
2842
2990
  },
2843
2991
  "operator": {
2844
2992
  "type": "string",
@@ -2973,7 +3121,8 @@
2973
3121
  {
2974
3122
  "name": "form_tokens",
2975
3123
  "in": "query",
2976
- "required": false
3124
+ "required": false,
3125
+ "array": true
2977
3126
  }
2978
3127
  ],
2979
3128
  "paramNames": [
@@ -2996,7 +3145,13 @@
2996
3145
  "description": "Search keyword (at most 200 characters), fuzzy-matched in the form's public searchable fields."
2997
3146
  },
2998
3147
  "form_tokens": {
2999
- "type": "string",
3148
+ "type": [
3149
+ "array",
3150
+ "string"
3151
+ ],
3152
+ "items": {
3153
+ "type": "string"
3154
+ },
3000
3155
  "description": "Search only these forms. Comma-separated form tokens, or an array parameter (`form_tokens[]=`)."
3001
3156
  }
3002
3157
  },
@@ -3937,13 +4092,22 @@
3937
4092
  "type": "object",
3938
4093
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
3939
4094
  "required": [
3940
- "field",
3941
4095
  "operator"
3942
4096
  ],
3943
4097
  "properties": {
3944
4098
  "field": {
3945
4099
  "type": "string",
3946
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
4100
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
4101
+ },
4102
+ "api_code": {
4103
+ "type": "string",
4104
+ "deprecated": true,
4105
+ "description": "Deprecated alias of `field`; use `field`."
4106
+ },
4107
+ "trigger": {
4108
+ "type": "string",
4109
+ "deprecated": true,
4110
+ "description": "Deprecated alias of `field`; use `field`."
3947
4111
  },
3948
4112
  "operator": {
3949
4113
  "type": "string",
@@ -5634,7 +5798,7 @@
5634
5798
  {
5635
5799
  "operationId": "createForm",
5636
5800
  "summary": "Create a form",
5637
- "description": "Creates an online exam form (scene=exam, scored automatically). Use it, not create_form, when the user wants an exam, test or quiz; an evaluation such as a psychological test, ability assessment, self-assessment questionnaire or training feedback uses create_evaluation_form. Questions must use exam question types (SingleSelect / MultiSelect / TrueOrFalse / DropDownSelect / FillInBlank / ShortAnswer / FillInNumber), each with answers (the correct answer and its score; the default absolute mode has one correct answer and scores only a fully correct response). Creates the form first, then applies exam_setting; if the settings are rejected, the form already exists and the result says so.\nCreates a form from a list of field definitions and returns the full form structure. `name` and `fields` are required.\n- `scene` sets the form's scene; exam / evaluation question types (such as `SingleSelect`, `FillInBlank`) can only be used in the scenes that support them.\n- `layout=card` (paged) does not support PageBreak, Matrix*, TableField, GoodsField, ESignatureField, FormAssociation, MultipleBlanksField or any kind of Widget field.\n- The folder named by `folder_token` must be manageable and of kind form; otherwise returns 404 / 400 respectively.\n- `setting` accepts only whitelisted keys (see `FormSettingWriteInput`); `setting.notification_rules` replaces, as a whole, the notification rules created through the API.\n- The returned form structure carries `url` (the public fill URL).",
5801
+ "description": "Creates an online exam form (scene=exam, scored automatically). Use it, not create_form, when the user wants an exam, test or quiz; an evaluation such as a psychological test, ability assessment, self-assessment questionnaire or training feedback uses create_evaluation_form. Questions must use exam question types (SingleSelect / MultiSelect / TrueOrFalse / DropDownSelect / FillInBlank / ShortAnswer / FillInNumber), each with answers (the correct answer and its score; the default absolute mode has one correct answer and scores only a fully correct response). A question type's schema is the one FieldInput's x-variants names (SingleSelect → \"SingleSelectInput\"; see get_schema). Creates the form first, then applies exam_setting; if the settings are rejected, the form already exists and the result says so.\nCreates a form from a list of field definitions and returns the full form structure. `name` and `fields` are required.\n- `scene` sets the form's scene; exam / evaluation question types (such as `SingleSelect`, `FillInBlank`) can only be used in the scenes that support them.\n- `layout=card` (paged) does not support PageBreak, Matrix*, TableField, GoodsField, ESignatureField, FormAssociation, MultipleBlanksField or any kind of Widget field.\n- The folder named by `folder_token` must be manageable and of kind form; otherwise returns 404 / 400 respectively.\n- `setting` accepts only whitelisted keys (see `FormSettingWriteInput`); `setting.notification_rules` replaces, as a whole, the notification rules created through the API.\n- The returned form structure carries `url` (the public fill URL).",
5638
5802
  "tag": "Forms",
5639
5803
  "method": "post",
5640
5804
  "path": "/forms",
@@ -6203,7 +6367,7 @@
6203
6367
  {
6204
6368
  "operationId": "createForm",
6205
6369
  "summary": "Create a form",
6206
- "description": "Creates an evaluation form (scene=evaluation). Use it when the user wants an evaluation, psychological test, ability assessment, self-assessment questionnaire or training feedback; not create_form, and not create_exam_form (an exam with correct answers). A scored evaluation uses SingleSelect / MultiSelect / DropDownSelect / Rating / Nps: each question lists every choice with its score in answers and sets answer_setting_mode=relative (every choice scores, none is right or wrong; the default absolute accepts one correct answer and scores any other choice 0). Do not score with a regular choice field's calculable / operand_value. A feedback questionnaire uses regular fields (RatingField / MatrixScaleField / TextArea, etc.) without scores. Creates the form first, then applies evaluation_setting (the report after submission, comments by total score range, dimensions).\nCreates a form from a list of field definitions and returns the full form structure. `name` and `fields` are required.\n- `scene` sets the form's scene; exam / evaluation question types (such as `SingleSelect`, `FillInBlank`) can only be used in the scenes that support them.\n- `layout=card` (paged) does not support PageBreak, Matrix*, TableField, GoodsField, ESignatureField, FormAssociation, MultipleBlanksField or any kind of Widget field.\n- The folder named by `folder_token` must be manageable and of kind form; otherwise returns 404 / 400 respectively.\n- `setting` accepts only whitelisted keys (see `FormSettingWriteInput`); `setting.notification_rules` replaces, as a whole, the notification rules created through the API.\n- The returned form structure carries `url` (the public fill URL).",
6370
+ "description": "Creates an evaluation form (scene=evaluation). Use it when the user wants an evaluation, psychological test, ability assessment, self-assessment questionnaire or training feedback; not create_form, and not create_exam_form (an exam with correct answers). A scored evaluation uses SingleSelect / MultiSelect / DropDownSelect / Rating / Nps: each question lists every choice with its score in answers and sets answer_setting_mode=relative (every choice scores, none is right or wrong; the default absolute accepts one correct answer and scores any other choice 0). A question type's schema is the one FieldInput's x-variants names (SingleSelect → \"SingleSelectInput\"; see get_schema). Do not score with a regular choice field's calculable / operand_value. A feedback questionnaire uses regular fields (RatingField / MatrixScaleField / TextArea, etc.) without scores. Creates the form first, then applies evaluation_setting (the report after submission, comments by total score range, dimensions).\nCreates a form from a list of field definitions and returns the full form structure. `name` and `fields` are required.\n- `scene` sets the form's scene; exam / evaluation question types (such as `SingleSelect`, `FillInBlank`) can only be used in the scenes that support them.\n- `layout=card` (paged) does not support PageBreak, Matrix*, TableField, GoodsField, ESignatureField, FormAssociation, MultipleBlanksField or any kind of Widget field.\n- The folder named by `folder_token` must be manageable and of kind form; otherwise returns 404 / 400 respectively.\n- `setting` accepts only whitelisted keys (see `FormSettingWriteInput`); `setting.notification_rules` replaces, as a whole, the notification rules created through the API.\n- The returned form structure carries `url` (the public fill URL).",
6207
6371
  "tag": "Forms",
6208
6372
  "method": "post",
6209
6373
  "path": "/forms",
@@ -6725,13 +6889,22 @@
6725
6889
  "type": "object",
6726
6890
  "description": "One filter condition. A `filters` parameter is the JSON text of an array of these, combined with AND; each endpoint says which fields and operators it takes.",
6727
6891
  "required": [
6728
- "field",
6729
6892
  "operator"
6730
6893
  ],
6731
6894
  "properties": {
6732
6895
  "field": {
6733
6896
  "type": "string",
6734
- "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists"
6897
+ "description": "What to filter on; a field api_code for entries, or a form attribute such as `created_at` for form lists. Required unless the deprecated `api_code` or `trigger` is given instead."
6898
+ },
6899
+ "api_code": {
6900
+ "type": "string",
6901
+ "deprecated": true,
6902
+ "description": "Deprecated alias of `field`; use `field`."
6903
+ },
6904
+ "trigger": {
6905
+ "type": "string",
6906
+ "deprecated": true,
6907
+ "description": "Deprecated alias of `field`; use `field`."
6735
6908
  },
6736
6909
  "operator": {
6737
6910
  "type": "string",
package/dist/server.js CHANGED
@@ -12,7 +12,7 @@ export const INSTRUCTIONS = [
12
12
  'MCP server for Jinshuju (金数据) forms, tables and entries. Each tool maps to the open API v1 and is generated from its openapi.yaml.',
13
13
  "Usual path: list_forms finds the form token → get_form shows the fields' api_code → list_entries / count_entries / aggregate_entries read entries;",
14
14
  'create_entry / patch_entries write them, keyed by api_code.',
15
- 'To create a form: get_schema("FieldInput") lists the field types, get_schema("<Type>Input") shows a type\'s properties, then call create_form.',
15
+ 'To create a form: get_schema("FieldInput") maps each field type to its schema in x-variants (TextField → TextFieldInput, SingleSelect → SingleSelectInput), get_schema on that name shows the type\'s properties, then call create_form.',
16
16
  'Wherever an input says `Shape: schema "X"`, look X up with get_schema; do not guess.',
17
17
  "An error gives the HTTP status and the server's error_description / errors; fix the call as they say and retry. On 429, wait as Retry-After says.",
18
18
  'A personal token sees only the forms its user created or that are shared with them; an account token sees every form in the account.'
package/dist/tools.js CHANGED
@@ -47,6 +47,40 @@ export function registerGeneratedTools(server, runtime) {
47
47
  export function schemaNames() {
48
48
  return Object.keys(SCHEMAS);
49
49
  }
50
+ /** Lower-case words of a schema or tool name, without Input; edit reads as update, since tools say edit where schemas say Update. */
51
+ function nameWords(name) {
52
+ const words = name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|\d+/g) ?? [];
53
+ return new Set(words
54
+ .map((word) => word.toLowerCase())
55
+ .filter((word) => word !== 'input')
56
+ .map((word) => (word === 'edit' ? 'update' : word)));
57
+ }
58
+ /** Schema names containing the name come first, then those sharing the most words with it. */
59
+ function similarSchemaNames(name) {
60
+ const lower = name.toLowerCase();
61
+ const words = nameWords(name);
62
+ const score = (candidate) => {
63
+ if (candidate.toLowerCase().includes(lower))
64
+ return 2;
65
+ const other = nameWords(candidate);
66
+ const shared = [...words].filter((word) => other.has(word)).length;
67
+ return shared / new Set([...words, ...other]).size;
68
+ };
69
+ return schemaNames()
70
+ .map((candidate) => ({ candidate, score: score(candidate) }))
71
+ .filter((scored) => scored.score > 0)
72
+ .sort((a, b) => b.score - a.score)
73
+ .slice(0, 5)
74
+ .map((scored) => scored.candidate);
75
+ }
76
+ /** get_schema's answer to an unknown name: a guessed tool input (EditFormInput) is pointed back at the tool, anything else gets similar names. */
77
+ function unknownSchemaMessage(name) {
78
+ const tool = TOOLS.find((t) => `${t.name.replaceAll('_', '')}input` === name.toLowerCase());
79
+ if (tool)
80
+ return `No schema named ${name}. ${tool.name} takes the arguments listed in its own inputSchema; there is no schema to look up for them.`;
81
+ const candidates = similarSchemaNames(name);
82
+ return `No schema named ${name}.${candidates.length ? ` Similar names: ${candidates.join(', ')}` : ''}`;
83
+ }
50
84
  export function registerSchemaTools(server, pointerSchemas) {
51
85
  server.registerTool('get_schema', {
52
86
  title: 'Show a schema',
@@ -56,7 +90,10 @@ export function registerSchemaTools(server, pointerSchemas) {
56
90
  inputSchema: fromJsonSchema({
57
91
  type: 'object',
58
92
  properties: {
59
- name: { type: 'string', description: 'Schema name, e.g. FieldInput, TextFieldInput, FormSettingWriteInput' }
93
+ name: {
94
+ type: 'string',
95
+ description: 'Schema name, e.g. FieldInput, TextFieldInput, SingleSelectInput, FormSettingWriteInput'
96
+ }
60
97
  },
61
98
  required: ['name'],
62
99
  additionalProperties: false
@@ -64,20 +101,8 @@ export function registerSchemaTools(server, pointerSchemas) {
64
101
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
65
102
  }, async ({ name }) => {
66
103
  const schema = SCHEMAS[name];
67
- if (!schema) {
68
- const candidates = schemaNames()
69
- .filter((n) => n.toLowerCase().includes(name.toLowerCase()))
70
- .slice(0, 10);
71
- return {
72
- isError: true,
73
- content: [
74
- {
75
- type: 'text',
76
- text: `No schema named ${name}.${candidates.length ? ` Similar names: ${candidates.join(', ')}` : ''}`
77
- }
78
- ]
79
- };
80
- }
104
+ if (!schema)
105
+ return { isError: true, content: [{ type: 'text', text: unknownSchemaMessage(name) }] };
81
106
  return { content: [{ type: 'text', text: JSON.stringify(schema) }] };
82
107
  });
83
108
  }
package/dist/version.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export declare const VERSION = "0.3.0";
1
+ export declare const VERSION = "0.4.0";
2
2
  export declare const SERVER_NAME = "jinshuju";
3
3
  export declare const SERVER_TITLE = "Jinshuju MCP Server";
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
- // release-please keeps this in sync through extra-files; the runtime does not read package.json because Workers has no file system.
2
- export const VERSION = '0.3.0'; // x-release-please-version
1
+ // scripts/release.mjs keeps this in step with package.json; the runtime does not read package.json because Workers has no file system.
2
+ export const VERSION = '0.4.0'; // x-release-version
3
3
  export const SERVER_NAME = 'jinshuju';
4
4
  export const SERVER_TITLE = 'Jinshuju MCP Server';
5
5
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinshuju/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for the Jinshuju (金数据) Open API v1, generated from openapi.yaml",
5
5
  "homepage": "https://github.com/jinshuju/mcp#readme",
6
6
  "bugs": {
@@ -66,7 +66,7 @@
66
66
  "oxfmt": "^0.72.0",
67
67
  "oxlint": "^1.87.0",
68
68
  "tsx": "^4.23.0",
69
- "typescript": "^5.9.3",
69
+ "typescript": "^7.0.2",
70
70
  "wrangler": "^4.149.0",
71
71
  "yaml": "^2.9.1"
72
72
  },