@seaxlab/archery-mcp 1.0.0 → 1.2.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
@@ -1,6 +1,6 @@
1
1
  # Archery MCP
2
2
 
3
- 基于 MCP stdio、通过 Archery HTTP 接口访问内部 Archery。
3
+ 基于 MCP stdio、通过 Archery HTTP 接口访问 Archery。
4
4
 
5
5
  ## 前置条件
6
6
 
@@ -20,11 +20,27 @@
20
20
  | `ARCHERY_METADATA_CACHE_ENABLED` | 否 | `true` | 是否启用实例/库元数据缓存 |
21
21
  | `ARCHERY_METADATA_CACHE_TTL_SECONDS` | 否 | `604800` | 缓存有效期(秒),默认 7 天 |
22
22
  | `ARCHERY_METADATA_CACHE_PATH` | 否 | `~/.cache/archery-mcp/metadata-cache.json` | 缓存文件路径 |
23
+ | `ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_NAMES` | 否 | 空 | 不写入缓存的实例名黑名单,多个用英文逗号分隔 |
24
+ | `ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_IDS` | 否 | 空 | 不写入缓存的实例 ID 黑名单,多个用英文逗号分隔 |
23
25
 
24
26
  ## MCP 客户端配置
25
27
 
26
28
  在客户端的 MCP 配置里为进程设置与上表相同的 `env`。若使用已发布或已安装的包,可按下述配置。
27
29
 
30
+ ## 元数据缓存
31
+
32
+ `archery_list_instances` 和 `resource_type=database` 的 `archery_list_resources` 默认使用本地缓存。实例列表会先从 Archery 全量拉取,再在写入本地缓存前按 `ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_NAMES` 和 `ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_IDS` 排除黑名单实例。
33
+
34
+ 刷新数据库列表缓存时,如果没有显式指定 `instance_id` 或 `instance_name`,也会基于过滤后的实例列表刷新,避免对黑名单实例调用库列表接口。显式指定实例时按入参执行,便于临时排查单个实例。
35
+
36
+ 默认缓存路径:
37
+
38
+ ```text
39
+ ~/.cache/archery-mcp/metadata-cache.json
40
+ ```
41
+
42
+ 默认缓存有效期为 7 天。需要实时刷新时,可以在工具入参中传 `force_refresh=true`,也可以调用 `archery_refresh_metadata_cache`。
43
+
28
44
  ### 使用已安装的包(推荐)
29
45
 
30
46
  全局安装或作为项目依赖安装 `@seaxlab/archery-mcp` 后,可用 `npx` 拉起(会使用包内 `bin`):
@@ -40,10 +56,11 @@
40
56
  "ARCHERY_USERNAME": "mcp_service",
41
57
  "ARCHERY_PASSWORD": "replace-me",
42
58
  "ARCHERY_DEFAULT_LIMIT": "100",
43
- "ARCHERY_MAX_LIMIT": "500"
59
+ "ARCHERY_MAX_LIMIT": "500",
60
+ "ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_NAMES": "instance-a,instance-b",
61
+ "ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_IDS": "1,2"
44
62
  }
45
63
  }
46
64
  }
47
65
  }
48
66
  ```
49
-
@@ -9,7 +9,8 @@ export interface QueryInput {
9
9
  tb_name?: string;
10
10
  }
11
11
  export interface ResourceInput {
12
- instance_id: number;
12
+ instance_id?: number;
13
+ instance_name?: string;
13
14
  resource_type: "database" | "schema" | "table" | "column";
14
15
  db_name?: string;
15
16
  schema_name?: string;
@@ -1 +1 @@
1
- import{CookieJar as h}from"./cookie-jar.js";import{ArcheryMcpError as n}from"./errors.js";export class ArcheryClient{config;jar=new h;authenticated=!1;constructor(t){this.config=t}async ping(){return await this.ensureAuthenticated(),{ok:!0,baseUrl:this.config.ARCHERY_BASE_URL,authenticated:this.authenticated}}async listInstances(){return this.requestJson("/api/v1/instance/",{method:"GET"})}async listResources(t){return this.requestJson("/api/v1/instance/resource/",{method:"POST",form:{instance_id:String(t.instance_id),resource_type:t.resource_type,db_name:t.db_name??"",schema_name:t.schema_name??"",tb_name:t.tb_name??""}})}async query(t){return this.requestJson("/query/",{method:"POST",form:{instance_name:t.instance_name,db_name:t.db_name,sql_content:t.sql_content,limit_num:String(t.limit_num),schema_name:t.schema_name??"",tb_name:t.tb_name??""}})}async queryLogs(t){const e=new URLSearchParams;for(const[s,a]of Object.entries(t))a!==void 0&&e.set(s,String(a));const r=e.size>0?`?${e.toString()}`:"";return this.requestJson(`/query/querylog/${r}`,{method:"GET"})}async ensureAuthenticated(){this.authenticated&&this.jar.hasSession()||await this.login()}async login(){this.authenticated=!1,this.jar.clearSession();const t=await fetch(this.url("/login/"),{method:"GET",redirect:"manual"});this.jar.setFromHeaders(t.headers);const e=this.jar.get("csrftoken");if(!e)throw new n("ARCHERY_AUTH_FAILED","\u65E0\u6CD5\u4ECE Archery \u767B\u5F55\u9875\u83B7\u53D6 csrftoken",t.status);const r=new URLSearchParams({username:this.config.ARCHERY_USERNAME,password:this.config.ARCHERY_PASSWORD}),s=await fetch(this.url("/authenticate/"),{method:"POST",headers:this.headers({"Content-Type":"application/x-www-form-urlencoded","X-CSRFToken":e}),body:r.toString(),redirect:"manual"});this.jar.setFromHeaders(s.headers);const a=await u(s);if(!s.ok||a.status!==0)throw new n("ARCHERY_AUTH_FAILED",typeof a.msg=="string"?a.msg:"Archery \u8D26\u53F7\u5BC6\u7801\u767B\u5F55\u5931\u8D25",s.status);if(a.data)throw new n("ARCHERY_AUTH_FAILED","Archery \u8FD4\u56DE 2FA \u4F1A\u8BDD\uFF0C\u4F46\u8BE5 MCP \u65B9\u6848\u8981\u6C42\u670D\u52A1\u8D26\u53F7\u5173\u95ED 2FA",s.status);if(!this.jar.hasSession())throw new n("ARCHERY_AUTH_FAILED","Archery \u767B\u5F55\u6210\u529F\u4F46\u672A\u8FD4\u56DE sessionid",s.status);this.authenticated=!0}async requestJson(t,e){await this.ensureAuthenticated();const r=await this.rawRequest(t,e);if(this.looksUnauthenticated(r.response,r.text)){await this.login();const s=await this.rawRequest(t,e);return this.parseSuccessfulJson(s.response,s.text)}return this.parseSuccessfulJson(r.response,r.text)}async rawRequest(t,e){const r=this.jar.get("csrftoken")??"",s={};let a;e.method==="POST"&&(s["Content-Type"]="application/x-www-form-urlencoded",s["X-CSRFToken"]=r,a=new URLSearchParams(e.form??{}).toString());const o=await fetch(this.url(t),{method:e.method,headers:this.headers(s),body:a,redirect:"manual"});this.jar.setFromHeaders(o.headers);const c=await o.text();return{response:o,text:c}}parseSuccessfulJson(t,e){if(t.status===403)throw new n("ARCHERY_PERMISSION_DENIED","Archery \u62D2\u7EDD\u8BBF\u95EE\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u8D26\u53F7\u6743\u9650",t.status);if(!t.ok)throw new n("ARCHERY_HTTP_ERROR",`Archery HTTP ${t.status}: ${e.slice(0,200)}`,t.status);try{return JSON.parse(e)}catch{throw new n("ARCHERY_RESPONSE_PARSE_ERROR",`Archery \u8FD4\u56DE\u975E JSON \u5185\u5BB9: ${e.slice(0,200)}`,t.status)}}looksUnauthenticated(t,e){return t.status===401?!0:t.status>=300&&t.status<400?(t.headers.get("location")??"").includes("/login"):t.status===200&&e.includes("Login To Archery")}headers(t){const e=new Headers(t),r=this.jar.header();return r&&e.set("Cookie",r),e.set("Accept","application/json, text/plain, */*"),e}url(t){if(t.startsWith("http://")||t.startsWith("https://"))return t;const e=t.startsWith("/")?t:`/${t}`;return`${this.config.ARCHERY_BASE_URL}${e}`}}async function u(i){const t=await i.text();try{return JSON.parse(t)}catch{throw new n("ARCHERY_RESPONSE_PARSE_ERROR",`Archery \u8FD4\u56DE\u975E JSON \u5185\u5BB9: ${t.slice(0,200)}`,i.status)}}
1
+ import{CookieJar as l}from"./cookie-jar.js";import{ArcheryMcpError as n}from"./errors.js";export class ArcheryClient{config;jar=new l;authenticated=!1;constructor(e){this.config=e}async ping(){return await this.ensureAuthenticated(),{ok:!0,baseUrl:this.config.ARCHERY_BASE_URL,authenticated:this.authenticated}}async listInstances(){return this.requestJson("/group/user_all_instances/",{method:"GET",form:{"tag_codes[]":"can_read"}})}async listResources(e){return this.requestJson("/instance/instance_resource/",{method:"GET",form:{instance_id:e.instance_id===void 0?"":String(e.instance_id),instance_name:e.instance_name??"",resource_type:e.resource_type,db_name:e.db_name??"",schema_name:e.schema_name??"",tb_name:e.tb_name??""}})}async query(e){return this.requestJson("/query/",{method:"POST",form:{instance_name:e.instance_name,db_name:e.db_name,sql_content:e.sql_content,limit_num:String(e.limit_num),schema_name:e.schema_name??"",tb_name:e.tb_name??""}})}async queryLogs(e){const t=new URLSearchParams;for(const[s,a]of Object.entries(e))a!==void 0&&t.set(s,String(a));const r=t.size>0?`?${t.toString()}`:"";return this.requestJson(`/query/querylog/${r}`,{method:"GET"})}async ensureAuthenticated(){this.authenticated&&this.jar.hasSession()||await this.login()}async login(){this.authenticated=!1,this.jar.clearSession();const e=await fetch(this.url("/login/"),{method:"GET",redirect:"manual"});this.jar.setFromHeaders(e.headers);const t=this.jar.get("csrftoken");if(!t)throw new n("ARCHERY_AUTH_FAILED","\u65E0\u6CD5\u4ECE Archery \u767B\u5F55\u9875\u83B7\u53D6 csrftoken",e.status);const r=new URLSearchParams({username:this.config.ARCHERY_USERNAME,password:this.config.ARCHERY_PASSWORD}),s=await fetch(this.url("/authenticate/"),{method:"POST",headers:this.headers({"Content-Type":"application/x-www-form-urlencoded","X-CSRFToken":t}),body:r.toString(),redirect:"manual"});this.jar.setFromHeaders(s.headers);const a=await R(s);if(!s.ok||a.status!==0)throw new n("ARCHERY_AUTH_FAILED",typeof a.msg=="string"?a.msg:"Archery \u8D26\u53F7\u5BC6\u7801\u767B\u5F55\u5931\u8D25",s.status);if(a.data)throw new n("ARCHERY_AUTH_FAILED","Archery \u8FD4\u56DE 2FA \u4F1A\u8BDD\uFF0C\u4F46\u8BE5 MCP \u65B9\u6848\u8981\u6C42\u670D\u52A1\u8D26\u53F7\u5173\u95ED 2FA",s.status);if(!this.jar.hasSession())throw new n("ARCHERY_AUTH_FAILED","Archery \u767B\u5F55\u6210\u529F\u4F46\u672A\u8FD4\u56DE sessionid",s.status);this.authenticated=!0}async requestJson(e,t){await this.ensureAuthenticated();const r=await this.rawRequest(e,t);if(this.looksUnauthenticated(r.response,r.text)){await this.login();const s=await this.rawRequest(e,t);return this.parseSuccessfulJson(s.response,s.text)}return this.parseSuccessfulJson(r.response,r.text)}async rawRequest(e,t){const r=this.jar.get("csrftoken")??"",s={};let a,i=e;if(t.method==="POST")s["Content-Type"]="application/x-www-form-urlencoded",s["X-CSRFToken"]=r,a=new URLSearchParams(t.form??{}).toString();else if(t.form&&Object.keys(t.form).length>0){const h=new URLSearchParams;for(const[f,u]of Object.entries(t.form))u!==""&&h.set(f,u);const m=i.includes("?")?"&":"?";i=h.size>0?`${i}${m}${h.toString()}`:i}const o=await fetch(this.url(i),{method:t.method,headers:this.headers(s),body:a,redirect:"manual"});this.jar.setFromHeaders(o.headers);const d=await o.text();return{response:o,text:d}}parseSuccessfulJson(e,t){if(e.status===403)throw new n("ARCHERY_PERMISSION_DENIED","Archery \u62D2\u7EDD\u8BBF\u95EE\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u8D26\u53F7\u6743\u9650",e.status);if(!e.ok)throw new n("ARCHERY_HTTP_ERROR",`Archery HTTP ${e.status}: ${t.slice(0,200)}`,e.status);try{return JSON.parse(t)}catch{throw new n("ARCHERY_RESPONSE_PARSE_ERROR",`Archery \u8FD4\u56DE\u975E JSON \u5185\u5BB9: ${t.slice(0,200)}`,e.status)}}looksUnauthenticated(e,t){return e.status===401?!0:e.status>=300&&e.status<400?(e.headers.get("location")??"").includes("/login"):e.status===200&&t.includes("Login To Archery")}headers(e){const t=new Headers(e),r=this.jar.header();return r&&t.set("Cookie",r),t.set("Accept","application/json, text/plain, */*"),t}url(e){if(e.startsWith("http://")||e.startsWith("https://"))return e;const t=e.startsWith("/")?e:`/${e}`;return`${this.config.ARCHERY_BASE_URL}${t}`}}async function R(c){const e=await c.text();try{return JSON.parse(e)}catch{throw new n("ARCHERY_RESPONSE_PARSE_ERROR",`Archery \u8FD4\u56DE\u975E JSON \u5185\u5BB9: ${e.slice(0,200)}`,c.status)}}
package/dist/config.d.ts CHANGED
@@ -8,6 +8,8 @@ declare const envSchema: z.ZodObject<{
8
8
  ARCHERY_METADATA_CACHE_ENABLED: z.ZodDefault<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
9
9
  ARCHERY_METADATA_CACHE_TTL_SECONDS: z.ZodDefault<z.ZodNumber>;
10
10
  ARCHERY_METADATA_CACHE_PATH: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
11
+ ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_NAMES: z.ZodEffects<z.ZodOptional<z.ZodString>, string[], string | undefined>;
12
+ ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_IDS: z.ZodEffects<z.ZodOptional<z.ZodString>, number[], string | undefined>;
11
13
  }, "strip", z.ZodTypeAny, {
12
14
  ARCHERY_BASE_URL: string;
13
15
  ARCHERY_USERNAME: string;
@@ -16,6 +18,8 @@ declare const envSchema: z.ZodObject<{
16
18
  ARCHERY_MAX_LIMIT: number;
17
19
  ARCHERY_METADATA_CACHE_ENABLED: boolean;
18
20
  ARCHERY_METADATA_CACHE_TTL_SECONDS: number;
21
+ ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_NAMES: string[];
22
+ ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_IDS: number[];
19
23
  ARCHERY_METADATA_CACHE_PATH?: string | undefined;
20
24
  }, {
21
25
  ARCHERY_BASE_URL: string;
@@ -26,6 +30,8 @@ declare const envSchema: z.ZodObject<{
26
30
  ARCHERY_METADATA_CACHE_ENABLED?: unknown;
27
31
  ARCHERY_METADATA_CACHE_TTL_SECONDS?: number | undefined;
28
32
  ARCHERY_METADATA_CACHE_PATH?: string | undefined;
33
+ ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_NAMES?: string | undefined;
34
+ ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_IDS?: string | undefined;
29
35
  }>;
30
36
  export type ArcheryMcpConfig = z.infer<typeof envSchema>;
31
37
  export declare function loadConfig(env?: NodeJS.ProcessEnv): ArcheryMcpConfig;
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{z as n}from"zod";const A=n.preprocess(e=>{if(!(e===void 0||e==="")){if(typeof e=="boolean")return e;if(typeof e=="string"){const r=e.trim().toLowerCase();if(["true","1","yes","y","on"].includes(r))return!0;if(["false","0","no","n","off"].includes(r))return!1}return e}},n.boolean()).default(!0),i=n.string().optional().transform(e=>{const r=e?.trim();return r||void 0}),R=n.object({ARCHERY_BASE_URL:n.string().url(),ARCHERY_USERNAME:n.string().min(1),ARCHERY_PASSWORD:n.string().min(1),ARCHERY_DEFAULT_LIMIT:n.coerce.number().int().positive().default(100),ARCHERY_MAX_LIMIT:n.coerce.number().int().positive().default(500),ARCHERY_METADATA_CACHE_ENABLED:A,ARCHERY_METADATA_CACHE_TTL_SECONDS:n.coerce.number().int().positive().default(604800),ARCHERY_METADATA_CACHE_PATH:i});export function loadConfig(e=process.env){const r=R.safeParse(e);if(!r.success){const s=r.error.issues.map(t=>`${t.path.join(".")}: ${t.message}`).join("; ");throw new Error(`ARCHERY_CONFIG_MISSING: ${s}`)}const o=r.data;if(o.ARCHERY_DEFAULT_LIMIT>o.ARCHERY_MAX_LIMIT)throw new Error("ARCHERY_CONFIG_MISSING: ARCHERY_DEFAULT_LIMIT must be less than or equal to ARCHERY_MAX_LIMIT");return{...o,ARCHERY_BASE_URL:o.ARCHERY_BASE_URL.replace(/\/+$/,"")}}
1
+ import{z as e}from"zod";const i=e.preprocess(n=>{if(!(n===void 0||n==="")){if(typeof n=="boolean")return n;if(typeof n=="string"){const r=n.trim().toLowerCase();if(["true","1","yes","y","on"].includes(r))return!0;if(["false","0","no","n","off"].includes(r))return!1}return n}},e.boolean()).default(!0),E=e.string().optional().transform(n=>{const r=n?.trim();return r||void 0}),A=e.string().optional().transform(n=>(n??"").split(",").map(r=>r.trim()).filter(Boolean)),a=e.string().optional().transform((n,r)=>(n??"").split(",").map(t=>t.trim()).filter(Boolean).map(t=>{if(!/^\d+$/.test(t))return r.addIssue({code:e.ZodIssueCode.custom,message:`Invalid positive integer: ${t}`}),e.NEVER;const o=Number(t);return!Number.isSafeInteger(o)||o<=0?(r.addIssue({code:e.ZodIssueCode.custom,message:`Invalid positive integer: ${t}`}),e.NEVER):o})),_=e.object({ARCHERY_BASE_URL:e.string().url(),ARCHERY_USERNAME:e.string().min(1),ARCHERY_PASSWORD:e.string().min(1),ARCHERY_DEFAULT_LIMIT:e.coerce.number().int().positive().default(100),ARCHERY_MAX_LIMIT:e.coerce.number().int().positive().default(500),ARCHERY_METADATA_CACHE_ENABLED:i,ARCHERY_METADATA_CACHE_TTL_SECONDS:e.coerce.number().int().positive().default(604800),ARCHERY_METADATA_CACHE_PATH:E,ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_NAMES:A,ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_IDS:a});export function loadConfig(n=process.env){const r=_.safeParse(n);if(!r.success){const o=r.error.issues.map(s=>`${s.path.join(".")}: ${s.message}`).join("; ");throw new Error(`ARCHERY_CONFIG_MISSING: ${o}`)}const t=r.data;if(t.ARCHERY_DEFAULT_LIMIT>t.ARCHERY_MAX_LIMIT)throw new Error("ARCHERY_CONFIG_MISSING: ARCHERY_DEFAULT_LIMIT must be less than or equal to ARCHERY_MAX_LIMIT");return{...t,ARCHERY_BASE_URL:t.ARCHERY_BASE_URL.replace(/\/+$/,"")}}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Server as c}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as i}from"@modelcontextprotocol/sdk/server/stdio.js";import{ArcheryClient as s}from"./archery-client.js";import{loadConfig as m}from"./config.js";import{MetadataCache as p}from"./metadata-cache.js";import{MetadataService as f}from"./metadata-service.js";import{registerTools as l}from"./tools.js";async function d(){const t=m(),e=new s(t),r=new p(t),a=new f(e,r),o=new c({name:"archery-mcp",version:"0.1.0"},{capabilities:{tools:{}}});l(o,e,t,a);const n=new i;await o.connect(n)}d().catch(t=>{const e=t instanceof Error?t.message:String(t);console.error(`archery-mcp failed to start: ${e}`),process.exit(1)});
2
+ import{Server as c}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as i}from"@modelcontextprotocol/sdk/server/stdio.js";import{ArcheryClient as s}from"./archery-client.js";import{loadConfig as m}from"./config.js";import{MetadataCache as p}from"./metadata-cache.js";import{MetadataService as f}from"./metadata-service.js";import{registerTools as l}from"./tools.js";async function d(){const t=m(),e=new s(t),r=new p(t),a=new f(e,r,t),o=new c({name:"archery-mcp",version:"0.1.0"},{capabilities:{tools:{}}});l(o,e,t,a);const n=new i;await o.connect(n)}d().catch(t=>{const e=t instanceof Error?t.message:String(t);console.error(`archery-mcp failed to start: ${e}`),process.exit(1)});
@@ -20,8 +20,8 @@ export declare class MetadataCache {
20
20
  getPath(): string;
21
21
  readInstances(): Promise<CacheReadResult | undefined>;
22
22
  writeInstances(data: unknown): Promise<CacheReadResult>;
23
- readDatabases(instanceId: number): Promise<CacheReadResult | undefined>;
24
- writeDatabases(instanceId: number, data: unknown): Promise<CacheReadResult>;
23
+ readDatabases(instanceKey: number | string): Promise<CacheReadResult | undefined>;
24
+ writeDatabases(instanceKey: number | string, data: unknown): Promise<CacheReadResult>;
25
25
  realtimeState(): CacheState;
26
26
  private toReadResult;
27
27
  private toCacheState;
@@ -1,4 +1,5 @@
1
1
  import type { ArcheryClient } from "./archery-client.js";
2
+ import type { ArcheryMcpConfig } from "./config.js";
2
3
  import type { CacheState, MetadataCache } from "./metadata-cache.js";
3
4
  type RefreshScope = "instances" | "databases" | "all";
4
5
  export interface CacheOptions {
@@ -7,6 +8,11 @@ export interface CacheOptions {
7
8
  export interface RefreshOptions {
8
9
  scope?: RefreshScope;
9
10
  instance_id?: number;
11
+ instance_name?: string;
12
+ }
13
+ export interface DatabaseTarget {
14
+ instance_id?: number;
15
+ instance_name?: string;
10
16
  }
11
17
  export interface CachedPayload {
12
18
  cache: CacheState;
@@ -19,8 +25,7 @@ export interface RefreshPayload {
19
25
  };
20
26
  refreshed: {
21
27
  instances: boolean;
22
- databases: Array<{
23
- instance_id: number;
28
+ databases: Array<DatabaseTarget & {
24
29
  ok: boolean;
25
30
  message?: string;
26
31
  }>;
@@ -29,10 +34,13 @@ export interface RefreshPayload {
29
34
  export declare class MetadataService {
30
35
  private readonly client;
31
36
  private readonly cache;
32
- constructor(client: ArcheryClient, cache: MetadataCache);
37
+ private readonly excludedInstanceNames;
38
+ private readonly excludedInstanceIds;
39
+ constructor(client: ArcheryClient, cache: MetadataCache, config: ArcheryMcpConfig);
33
40
  listInstances(options?: CacheOptions): Promise<CachedPayload>;
34
- listDatabases(instanceId: number, options?: CacheOptions): Promise<CachedPayload>;
41
+ listDatabases(target: DatabaseTarget, options?: CacheOptions): Promise<CachedPayload>;
35
42
  refreshMetadataCache(options?: RefreshOptions): Promise<RefreshPayload>;
36
43
  private wrapRealtime;
44
+ private filterInstances;
37
45
  }
38
46
  export {};
@@ -1 +1 @@
1
- export class MetadataService{client;cache;constructor(t,a){this.client=t,this.cache=a}async listInstances(t={}){if(!this.cache.isEnabled())return this.wrapRealtime(await this.client.listInstances());if(!t.force_refresh){const e=await this.cache.readInstances();if(e)return e}const a=await this.client.listInstances();return this.cache.writeInstances(a)}async listDatabases(t,a={}){if(!this.cache.isEnabled())return this.wrapRealtime(await this.client.listResources({instance_id:t,resource_type:"database"}));if(!a.force_refresh){const s=await this.cache.readDatabases(t);if(s)return s}const e=await this.client.listResources({instance_id:t,resource_type:"database"});return this.cache.writeDatabases(t,e)}async refreshMetadataCache(t={}){const a=t.scope??"all",e={instances:!1,databases:[]};let s;if((a==="instances"||a==="all")&&(s=await this.client.listInstances(),this.cache.isEnabled()&&await this.cache.writeInstances(s),e.instances=!0),a==="databases"||a==="all"){const h=t.instance_id===void 0?d(s??await this.client.listInstances()):[t.instance_id];for(const n of h)try{const i=await this.client.listResources({instance_id:n,resource_type:"database"});this.cache.isEnabled()&&await this.cache.writeDatabases(n,i),e.databases.push({instance_id:n,ok:!0})}catch(i){e.databases.push({instance_id:n,ok:!1,message:i instanceof Error?i.message:String(i)})}}return{cache:{enabled:this.cache.isEnabled(),path:this.cache.getPath()},refreshed:e}}wrapRealtime(t){return{cache:this.cache.realtimeState(),data:t}}}function d(c){const t=new Set;return r(c,t),[...t].sort((a,e)=>a-e)}function r(c,t){if(Array.isArray(c)){for(const s of c)r(s,t);return}if(!c||typeof c!="object")return;const a=c,e=a.instance_id??a.instanceId??a.id;if(typeof e=="number"&&Number.isInteger(e)&&e>0&&t.add(e),typeof e=="string"&&/^\d+$/.test(e)){const s=Number(e);s>0&&t.add(s)}for(const s of["data","rows","result","list"])s in a&&r(a[s],t)}
1
+ export class MetadataService{client;cache;excludedInstanceNames;excludedInstanceIds;constructor(e,s,t){this.client=e,this.cache=s,this.excludedInstanceNames=new Set(t.ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_NAMES),this.excludedInstanceIds=new Set(t.ARCHERY_METADATA_CACHE_EXCLUDE_INSTANCE_IDS)}async listInstances(e={}){if(!this.cache.isEnabled())return this.wrapRealtime(await this.client.listInstances());if(!e.force_refresh){const t=await this.cache.readInstances();if(t)return t}const s=this.filterInstances(await this.client.listInstances());return this.cache.writeInstances(s)}async listDatabases(e,s={}){const t=h(e);if(!this.cache.isEnabled())return this.wrapRealtime(await this.client.listResources({...e,resource_type:"database"}));if(t&&!s.force_refresh){const a=await this.cache.readDatabases(t);if(a)return a}const i=await this.client.listResources({...e,resource_type:"database"});return t?this.cache.writeDatabases(t,i):this.wrapRealtime(i)}async refreshMetadataCache(e={}){const s=e.scope??"all",t={instances:!1,databases:[]};let i;if((s==="instances"||s==="all")&&(i=this.filterInstances(await this.client.listInstances()),this.cache.isEnabled()&&await this.cache.writeInstances(i),t.instances=!0),s==="databases"||s==="all"){const a=e.instance_id!==void 0||e.instance_name!==void 0?[{instance_id:e.instance_id,instance_name:e.instance_name}]:m(i??this.filterInstances(await this.client.listInstances()));for(const c of a)try{const r=await this.client.listResources({...c,resource_type:"database"}),o=h(c);this.cache.isEnabled()&&o&&await this.cache.writeDatabases(o,r),t.databases.push({...c,ok:!0})}catch(r){t.databases.push({...c,ok:!1,message:r instanceof Error?r.message:String(r)})}}return{cache:{enabled:this.cache.isEnabled(),path:this.cache.getPath()},refreshed:t}}wrapRealtime(e){return{cache:this.cache.realtimeState(),data:e}}filterInstances(e){return this.excludedInstanceNames.size===0&&this.excludedInstanceIds.size===0?e:d(e,this.excludedInstanceNames,this.excludedInstanceIds)}}function d(n,e,s){if(Array.isArray(n))return n.filter(a=>!u(a,e,s)).map(a=>d(a,e,s));if(!n||typeof n!="object")return n;const t=n,i={};for(const[a,c]of Object.entries(t))i[a]=d(c,e,s);return i}function u(n,e,s){if(!n||typeof n!="object")return!1;const t=n,i=t.instance_id??t.instanceId??t.id,a=t.instance_name??t.instanceName;if(typeof a=="string"&&e.has(a.trim()))return!0;const c=l(i);return c!==void 0&&s.has(c)}function l(n){if(typeof n=="number"&&Number.isSafeInteger(n)&&n>0)return n;if(typeof n=="string"&&/^\d+$/.test(n)){const e=Number(n);return Number.isSafeInteger(e)&&e>0?e:void 0}}function h(n){return n.instance_name||n.instance_id}function m(n){const e=new Map;return f(n,e),[...e.values()].sort((s,t)=>{const i=s.instance_name??"",a=t.instance_name??"";return i.localeCompare(a,"zh-Hans-CN")})}function f(n,e){if(Array.isArray(n)){for(const c of n)f(c,e);return}if(!n||typeof n!="object")return;const s=n,t=s.instance_id??s.instanceId??s.id,i=s.instance_name??s.instanceName,a={};if(typeof t=="number"&&Number.isInteger(t)&&t>0&&(a.instance_id=t),typeof t=="string"&&/^\d+$/.test(t)){const c=Number(t);c>0&&(a.instance_id=c)}typeof i=="string"&&i.trim()&&(a.instance_name=i),(a.instance_id!==void 0||a.instance_name!==void 0)&&e.set(a.instance_name??String(a.instance_id),a);for(const c of["data","rows","result","list"])c in s&&f(s[c],e)}
package/dist/tools.js CHANGED
@@ -1 +1 @@
1
- import{CallToolRequestSchema as _,ListToolsRequestSchema as h}from"@modelcontextprotocol/sdk/types.js";import{z as e}from"zod";import{toErrorPayload as y}from"./errors.js";import{assertReadOnlySql as d,normalizeLimit as l}from"./sql-guard.js";const b=e.object({instance_id:e.number().int().positive(),resource_type:e.enum(["database","schema","table","column"]),db_name:e.string().optional(),schema_name:e.string().optional(),tb_name:e.string().optional(),force_refresh:e.boolean().optional()}),f=e.object({force_refresh:e.boolean().optional()}),g=e.object({scope:e.enum(["instances","databases","all"]).optional(),instance_id:e.number().int().positive().optional()}),R=e.object({instance_name:e.string().min(1),db_name:e.string().min(1),sql_content:e.string().min(1),limit_num:e.number().int().positive().optional(),schema_name:e.string().optional(),tb_name:e.string().optional()}),q=e.object({page:e.number().int().positive().optional(),limit:e.number().int().positive().optional(),instance_name:e.string().optional(),db_name:e.string().optional(),username:e.string().optional()});export function registerTools(i,a,o,m){i.setRequestHandler(h,async()=>({tools:[{name:"archery_ping",description:"Check Archery MCP configuration, reachability, and login state.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"archery_list_instances",description:"List Archery instances. Uses local metadata cache by default and refreshes when force_refresh is true or cache is expired.",inputSchema:{type:"object",properties:{force_refresh:{type:"boolean"}},additionalProperties:!1}},{name:"archery_list_resources",description:"List databases, schemas, tables, or columns for an Archery instance. Database discovery uses local metadata cache by default.",inputSchema:{type:"object",required:["instance_id","resource_type"],properties:{instance_id:{type:"number",minimum:1},resource_type:{type:"string",enum:["database","schema","table","column"]},db_name:{type:"string"},schema_name:{type:"string"},tb_name:{type:"string"},force_refresh:{type:"boolean"}},additionalProperties:!1}},{name:"archery_query",description:"Execute read-only SQL through Archery. Archery still enforces permissions, masking, limits, and audit logs.",inputSchema:{type:"object",required:["instance_name","db_name","sql_content"],properties:{instance_name:{type:"string",minLength:1},db_name:{type:"string",minLength:1},sql_content:{type:"string",minLength:1},limit_num:{type:"number",minimum:1},schema_name:{type:"string"},tb_name:{type:"string"}},additionalProperties:!1}},{name:"archery_query_logs",description:"Fetch Archery query logs visible to the service account.",inputSchema:{type:"object",properties:{page:{type:"number",minimum:1},limit:{type:"number",minimum:1},instance_name:{type:"string"},db_name:{type:"string"},username:{type:"string"}},additionalProperties:!1}},{name:"archery_refresh_metadata_cache",description:"Refresh local Archery metadata cache for instances and database lists.",inputSchema:{type:"object",properties:{scope:{type:"string",enum:["instances","databases","all"]},instance_id:{type:"number",minimum:1}},additionalProperties:!1}}]})),i.setRequestHandler(_,async p=>{const r=p.params.name,s=p.params.arguments??{};try{if(r==="archery_ping")return n(await a.ping());if(r==="archery_list_instances"){const t=f.parse(s);return n(await m.listInstances(t))}if(r==="archery_list_resources"){const t=b.parse(s);if(t.resource_type==="database")return n(await m.listDatabases(t.instance_id,{force_refresh:t.force_refresh}));const{force_refresh:c,...u}=t;return n(await a.listResources(u))}if(r==="archery_query"){const t=R.parse(s);d(t.sql_content);const c=l(t.limit_num,o.ARCHERY_DEFAULT_LIMIT,o.ARCHERY_MAX_LIMIT);return n(await a.query({...t,limit_num:c}))}if(r==="archery_query_logs"){const t=q.parse(s),c=t.limit===void 0?void 0:l(t.limit,o.ARCHERY_DEFAULT_LIMIT,o.ARCHERY_MAX_LIMIT);return n(await a.queryLogs({...t,limit:c}))}if(r==="archery_refresh_metadata_cache"){const t=g.parse(s);return n(await m.refreshMetadataCache(t))}return n({ok:!1,error:{code:"ARCHERY_HTTP_ERROR",message:`Unknown tool: ${r}`}},!0)}catch(t){return n(y(t),!0)}})}function n(i,a=!1){return{content:[{type:"text",text:JSON.stringify(i,null,2)}],...a?{isError:!0}:{}}}
1
+ import{CallToolRequestSchema as h,ListToolsRequestSchema as d}from"@modelcontextprotocol/sdk/types.js";import{z as e}from"zod";import{toErrorPayload as y}from"./errors.js";import{assertReadOnlySql as f,normalizeLimit as l}from"./sql-guard.js";const b=e.object({instance_id:e.number().int().positive().optional(),instance_name:e.string().min(1).optional(),resource_type:e.enum(["database","schema","table","column"]),db_name:e.string().optional(),schema_name:e.string().optional(),tb_name:e.string().optional(),force_refresh:e.boolean().optional()}).refine(a=>a.instance_id!==void 0||a.instance_name!==void 0,{message:"instance_id or instance_name is required"}),g=e.object({force_refresh:e.boolean().optional()}),R=e.object({scope:e.enum(["instances","databases","all"]).optional(),instance_id:e.number().int().positive().optional(),instance_name:e.string().min(1).optional()}),q=e.object({instance_name:e.string().min(1),db_name:e.string().min(1),sql_content:e.string().min(1),limit_num:e.number().int().positive().optional(),schema_name:e.string().optional(),tb_name:e.string().optional()}),L=e.object({page:e.number().int().positive().optional(),limit:e.number().int().positive().optional(),instance_name:e.string().optional(),db_name:e.string().optional(),username:e.string().optional()});export function registerTools(a,i,o,m){a.setRequestHandler(d,async()=>({tools:[{name:"archery_ping",description:"Check Archery MCP configuration, reachability, and login state.",inputSchema:{type:"object",properties:{},additionalProperties:!1}},{name:"archery_list_instances",description:"List Archery readable instances. Uses local metadata cache by default and refreshes when force_refresh is true or cache is expired.",inputSchema:{type:"object",properties:{force_refresh:{type:"boolean"}},additionalProperties:!1}},{name:"archery_list_resources",description:"List databases, schemas, tables, or columns for an Archery instance. Database discovery uses local metadata cache by default.",inputSchema:{type:"object",required:["resource_type"],properties:{instance_id:{type:"number",minimum:1},instance_name:{type:"string",minLength:1},resource_type:{type:"string",enum:["database","schema","table","column"]},db_name:{type:"string"},schema_name:{type:"string"},tb_name:{type:"string"},force_refresh:{type:"boolean"}},additionalProperties:!1}},{name:"archery_query",description:"Execute read-only SQL through Archery. Archery still enforces permissions, masking, limits, and audit logs.",inputSchema:{type:"object",required:["instance_name","db_name","sql_content"],properties:{instance_name:{type:"string",minLength:1},db_name:{type:"string",minLength:1},sql_content:{type:"string",minLength:1},limit_num:{type:"number",minimum:1},schema_name:{type:"string"},tb_name:{type:"string"}},additionalProperties:!1}},{name:"archery_query_logs",description:"Fetch Archery query logs visible to the service account.",inputSchema:{type:"object",properties:{page:{type:"number",minimum:1},limit:{type:"number",minimum:1},instance_name:{type:"string"},db_name:{type:"string"},username:{type:"string"}},additionalProperties:!1}},{name:"archery_refresh_metadata_cache",description:"Refresh local Archery metadata cache for instances and database lists.",inputSchema:{type:"object",properties:{scope:{type:"string",enum:["instances","databases","all"]},instance_id:{type:"number",minimum:1},instance_name:{type:"string",minLength:1}},additionalProperties:!1}}]})),a.setRequestHandler(h,async p=>{const r=p.params.name,s=p.params.arguments??{};try{if(r==="archery_ping")return n(await i.ping());if(r==="archery_list_instances"){const t=g.parse(s);return n(await m.listInstances(t))}if(r==="archery_list_resources"){const t=b.parse(s);if(t.resource_type==="database"){const _={instance_id:t.instance_id,instance_name:t.instance_name};return n(await m.listDatabases(_,{force_refresh:t.force_refresh}))}const{force_refresh:c,...u}=t;return n(await i.listResources(u))}if(r==="archery_query"){const t=q.parse(s);f(t.sql_content);const c=l(t.limit_num,o.ARCHERY_DEFAULT_LIMIT,o.ARCHERY_MAX_LIMIT);return n(await i.query({...t,limit_num:c}))}if(r==="archery_query_logs"){const t=L.parse(s),c=t.limit===void 0?void 0:l(t.limit,o.ARCHERY_DEFAULT_LIMIT,o.ARCHERY_MAX_LIMIT);return n(await i.queryLogs({...t,limit:c}))}if(r==="archery_refresh_metadata_cache"){const t=R.parse(s);return n(await m.refreshMetadataCache(t))}return n({ok:!1,error:{code:"ARCHERY_HTTP_ERROR",message:`Unknown tool: ${r}`}},!0)}catch(t){return n(y(t),!0)}})}function n(a,i=!1){return{content:[{type:"text",text:JSON.stringify(a,null,2)}],...i?{isError:!0}:{}}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seaxlab/archery-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Archery MCP server (Node.js, stdio)",
6
6
  "main": "dist/index.js",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@modelcontextprotocol/sdk": "^1.25.0",
28
+ "@seaxlab/archery-mcp": "^1.1.0",
28
29
  "zod": "^3.25.0"
29
30
  },
30
31
  "devDependencies": {