@seaxlab/archery-mcp 1.0.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 ADDED
@@ -0,0 +1,49 @@
1
+ # Archery MCP
2
+
3
+ 基于 MCP stdio、通过 Archery HTTP 接口访问内部 Archery。
4
+
5
+ ## 前置条件
6
+
7
+ - Node.js 18+;运行环境能访问 `ARCHERY_BASE_URL`。
8
+
9
+ ## 环境变量
10
+
11
+ 通过进程环境变量注入(不要把真实密码写入仓库,可参考仓库内 `.env.example`):
12
+
13
+ | 变量 | 必填 | 默认值 | 说明 |
14
+ | --- | --- | --- | --- |
15
+ | `ARCHERY_BASE_URL` | 是 | 无 | Archery 地址,会自动去掉末尾 `/` |
16
+ | `ARCHERY_USERNAME` | 是 | 无 | 服务账号用户名 |
17
+ | `ARCHERY_PASSWORD` | 是 | 无 | 服务账号密码 |
18
+ | `ARCHERY_DEFAULT_LIMIT` | 否 | `100` | 查询默认返回条数上限 |
19
+ | `ARCHERY_MAX_LIMIT` | 否 | `500` | MCP 侧允许的最大返回条数 |
20
+ | `ARCHERY_METADATA_CACHE_ENABLED` | 否 | `true` | 是否启用实例/库元数据缓存 |
21
+ | `ARCHERY_METADATA_CACHE_TTL_SECONDS` | 否 | `604800` | 缓存有效期(秒),默认 7 天 |
22
+ | `ARCHERY_METADATA_CACHE_PATH` | 否 | `~/.cache/archery-mcp/metadata-cache.json` | 缓存文件路径 |
23
+
24
+ ## MCP 客户端配置
25
+
26
+ 在客户端的 MCP 配置里为进程设置与上表相同的 `env`。若使用已发布或已安装的包,可按下述配置。
27
+
28
+ ### 使用已安装的包(推荐)
29
+
30
+ 全局安装或作为项目依赖安装 `@seaxlab/archery-mcp` 后,可用 `npx` 拉起(会使用包内 `bin`):
31
+
32
+ ```json
33
+ {
34
+ "mcpServers": {
35
+ "archery": {
36
+ "command": "npx",
37
+ "args": ["-y", "@seaxlab/archery-mcp"],
38
+ "env": {
39
+ "ARCHERY_BASE_URL": "http://archery.internal",
40
+ "ARCHERY_USERNAME": "mcp_service",
41
+ "ARCHERY_PASSWORD": "replace-me",
42
+ "ARCHERY_DEFAULT_LIMIT": "100",
43
+ "ARCHERY_MAX_LIMIT": "500"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
@@ -0,0 +1,40 @@
1
+ import type { ArcheryMcpConfig } from "./config.js";
2
+ export type ArcheryJson = Record<string, unknown>;
3
+ export interface QueryInput {
4
+ instance_name: string;
5
+ db_name: string;
6
+ sql_content: string;
7
+ limit_num: number;
8
+ schema_name?: string;
9
+ tb_name?: string;
10
+ }
11
+ export interface ResourceInput {
12
+ instance_id: number;
13
+ resource_type: "database" | "schema" | "table" | "column";
14
+ db_name?: string;
15
+ schema_name?: string;
16
+ tb_name?: string;
17
+ }
18
+ export declare class ArcheryClient {
19
+ private readonly config;
20
+ private readonly jar;
21
+ private authenticated;
22
+ constructor(config: ArcheryMcpConfig);
23
+ ping(): Promise<{
24
+ ok: true;
25
+ baseUrl: string;
26
+ authenticated: boolean;
27
+ }>;
28
+ listInstances(): Promise<unknown>;
29
+ listResources(input: ResourceInput): Promise<unknown>;
30
+ query(input: QueryInput): Promise<unknown>;
31
+ queryLogs(params: Record<string, string | number | undefined>): Promise<unknown>;
32
+ private ensureAuthenticated;
33
+ private login;
34
+ private requestJson;
35
+ private rawRequest;
36
+ private parseSuccessfulJson;
37
+ private looksUnauthenticated;
38
+ private headers;
39
+ private url;
40
+ }
@@ -0,0 +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)}}
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ declare const envSchema: z.ZodObject<{
3
+ ARCHERY_BASE_URL: z.ZodString;
4
+ ARCHERY_USERNAME: z.ZodString;
5
+ ARCHERY_PASSWORD: z.ZodString;
6
+ ARCHERY_DEFAULT_LIMIT: z.ZodDefault<z.ZodNumber>;
7
+ ARCHERY_MAX_LIMIT: z.ZodDefault<z.ZodNumber>;
8
+ ARCHERY_METADATA_CACHE_ENABLED: z.ZodDefault<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
9
+ ARCHERY_METADATA_CACHE_TTL_SECONDS: z.ZodDefault<z.ZodNumber>;
10
+ ARCHERY_METADATA_CACHE_PATH: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ ARCHERY_BASE_URL: string;
13
+ ARCHERY_USERNAME: string;
14
+ ARCHERY_PASSWORD: string;
15
+ ARCHERY_DEFAULT_LIMIT: number;
16
+ ARCHERY_MAX_LIMIT: number;
17
+ ARCHERY_METADATA_CACHE_ENABLED: boolean;
18
+ ARCHERY_METADATA_CACHE_TTL_SECONDS: number;
19
+ ARCHERY_METADATA_CACHE_PATH?: string | undefined;
20
+ }, {
21
+ ARCHERY_BASE_URL: string;
22
+ ARCHERY_USERNAME: string;
23
+ ARCHERY_PASSWORD: string;
24
+ ARCHERY_DEFAULT_LIMIT?: number | undefined;
25
+ ARCHERY_MAX_LIMIT?: number | undefined;
26
+ ARCHERY_METADATA_CACHE_ENABLED?: unknown;
27
+ ARCHERY_METADATA_CACHE_TTL_SECONDS?: number | undefined;
28
+ ARCHERY_METADATA_CACHE_PATH?: string | undefined;
29
+ }>;
30
+ export type ArcheryMcpConfig = z.infer<typeof envSchema>;
31
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): ArcheryMcpConfig;
32
+ export {};
package/dist/config.js ADDED
@@ -0,0 +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(/\/+$/,"")}}
@@ -0,0 +1,8 @@
1
+ export declare class CookieJar {
2
+ private readonly cookies;
3
+ setFromHeaders(headers: Headers): void;
4
+ get(name: string): string | undefined;
5
+ header(): string;
6
+ clearSession(): void;
7
+ hasSession(): boolean;
8
+ }
@@ -0,0 +1 @@
1
+ export class CookieJar{cookies=new Map;setFromHeaders(e){const o=a(e);for(const c of o){const s=c.split(";")[0],i=s.indexOf("=");if(i<=0)continue;const n=s.slice(0,i).trim(),r=s.slice(i+1).trim();n&&r&&this.cookies.set(n,r)}}get(e){return this.cookies.get(e)}header(){return[...this.cookies.entries()].map(([e,o])=>`${e}=${o}`).join("; ")}clearSession(){this.cookies.delete("sessionid")}hasSession(){return!!this.cookies.get("sessionid")}}function a(t){const e=t;if(typeof e.getSetCookie=="function")return e.getSetCookie();const o=t.get("set-cookie");return o?l(o):[]}function l(t){return t.split(/,(?=\s*[^;,=\s]+=[^;,]+)/g).map(e=>e.trim()).filter(Boolean)}
@@ -0,0 +1,13 @@
1
+ export type ArcheryErrorCode = "ARCHERY_CONFIG_MISSING" | "ARCHERY_AUTH_FAILED" | "ARCHERY_SESSION_EXPIRED" | "ARCHERY_PERMISSION_DENIED" | "ARCHERY_QUERY_REJECTED" | "ARCHERY_HTTP_ERROR" | "ARCHERY_RESPONSE_PARSE_ERROR";
2
+ export declare class ArcheryMcpError extends Error {
3
+ readonly code: ArcheryErrorCode;
4
+ readonly status?: number;
5
+ constructor(code: ArcheryErrorCode, message: string, status?: number);
6
+ }
7
+ export declare function toErrorPayload(error: unknown): {
8
+ ok: false;
9
+ error: {
10
+ code: ArcheryErrorCode;
11
+ message: string;
12
+ };
13
+ };
package/dist/errors.js ADDED
@@ -0,0 +1 @@
1
+ export class ArcheryMcpError extends Error{code;status;constructor(s,r,o){super(r),this.name="ArcheryMcpError",this.code=s,this.status=o}}export function toErrorPayload(e){return e instanceof ArcheryMcpError?{ok:!1,error:{code:e.code,message:e.message}}:e instanceof Error&&e.message.startsWith("ARCHERY_CONFIG_MISSING:")?{ok:!1,error:{code:"ARCHERY_CONFIG_MISSING",message:e.message.replace(/^ARCHERY_CONFIG_MISSING:\s*/,"")}}:{ok:!1,error:{code:"ARCHERY_HTTP_ERROR",message:e instanceof Error?e.message:"Unknown Archery MCP error"}}}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
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)});
@@ -0,0 +1,33 @@
1
+ import type { ArcheryMcpConfig } from "./config.js";
2
+ export interface CacheState {
3
+ enabled: boolean;
4
+ hit: boolean;
5
+ stale: boolean;
6
+ cached_at?: string;
7
+ expires_at?: string;
8
+ }
9
+ export interface CacheReadResult {
10
+ data: unknown;
11
+ cache: CacheState;
12
+ }
13
+ export declare class MetadataCache {
14
+ private readonly enabled;
15
+ private readonly ttlSeconds;
16
+ private readonly baseUrl;
17
+ private readonly cachePath;
18
+ constructor(config: ArcheryMcpConfig);
19
+ isEnabled(): boolean;
20
+ getPath(): string;
21
+ readInstances(): Promise<CacheReadResult | undefined>;
22
+ writeInstances(data: unknown): Promise<CacheReadResult>;
23
+ readDatabases(instanceId: number): Promise<CacheReadResult | undefined>;
24
+ writeDatabases(instanceId: number, data: unknown): Promise<CacheReadResult>;
25
+ realtimeState(): CacheState;
26
+ private toReadResult;
27
+ private toCacheState;
28
+ private isStale;
29
+ private createEntry;
30
+ private readFile;
31
+ private writeFile;
32
+ private emptyFile;
33
+ }
@@ -0,0 +1 @@
1
+ import{mkdir as n,readFile as c,writeFile as h}from"node:fs/promises";import{dirname as l,join as d,resolve as o}from"node:path";import{homedir as u}from"node:os";const i=1;export class MetadataCache{enabled;ttlSeconds;baseUrl;cachePath;constructor(e){this.enabled=e.ARCHERY_METADATA_CACHE_ENABLED,this.ttlSeconds=e.ARCHERY_METADATA_CACHE_TTL_SECONDS,this.baseUrl=e.ARCHERY_BASE_URL,this.cachePath=A(e.ARCHERY_METADATA_CACHE_PATH)}isEnabled(){return this.enabled}getPath(){return this.cachePath}async readInstances(){const t=(await this.readFile()).instances;return this.toReadResult(t)}async writeInstances(e){const t=await this.readFile(),a=this.createEntry(e);return t.instances=a,await this.writeFile(t),{data:e,cache:this.toCacheState(a,!1)}}async readDatabases(e){const a=(await this.readFile()).databasesByInstanceId?.[String(e)];return this.toReadResult(a)}async writeDatabases(e,t){const a=await this.readFile(),s=this.createEntry(t);return a.databasesByInstanceId={...a.databasesByInstanceId??{},[String(e)]:s},await this.writeFile(a),{data:t,cache:this.toCacheState(s,!1)}}realtimeState(){return{enabled:this.enabled,hit:!1,stale:!1}}toReadResult(e){if(!(!e||this.isStale(e)))return{data:e.data,cache:this.toCacheState(e,!0)}}toCacheState(e,t){const a=Date.parse(e.cachedAt),s=new Date(a+this.ttlSeconds*1e3).toISOString();return{enabled:this.enabled,hit:t,stale:!1,cached_at:e.cachedAt,expires_at:s}}isStale(e){const t=Date.parse(e.cachedAt);return Number.isNaN(t)?!0:Date.now()-t>=this.ttlSeconds*1e3}createEntry(e){return{cachedAt:new Date().toISOString(),data:e}}async readFile(){try{const e=await c(this.cachePath,"utf8"),t=JSON.parse(e);return t.version!==i||t.baseUrl!==this.baseUrl?this.emptyFile():{...t,databasesByInstanceId:t.databasesByInstanceId??{}}}catch(e){if(e instanceof Error&&"code"in e&&e.code==="ENOENT")return this.emptyFile();if(e instanceof SyntaxError)return this.emptyFile();throw e}}async writeFile(e){await n(l(this.cachePath),{recursive:!0}),await h(this.cachePath,JSON.stringify(e,null,2),"utf8")}emptyFile(){return{version:i,baseUrl:this.baseUrl,databasesByInstanceId:{}}}}function A(r){return r?o(r):d(u(),".cache","archery-mcp","metadata-cache.json")}
@@ -0,0 +1,38 @@
1
+ import type { ArcheryClient } from "./archery-client.js";
2
+ import type { CacheState, MetadataCache } from "./metadata-cache.js";
3
+ type RefreshScope = "instances" | "databases" | "all";
4
+ export interface CacheOptions {
5
+ force_refresh?: boolean;
6
+ }
7
+ export interface RefreshOptions {
8
+ scope?: RefreshScope;
9
+ instance_id?: number;
10
+ }
11
+ export interface CachedPayload {
12
+ cache: CacheState;
13
+ data: unknown;
14
+ }
15
+ export interface RefreshPayload {
16
+ cache: {
17
+ enabled: boolean;
18
+ path: string;
19
+ };
20
+ refreshed: {
21
+ instances: boolean;
22
+ databases: Array<{
23
+ instance_id: number;
24
+ ok: boolean;
25
+ message?: string;
26
+ }>;
27
+ };
28
+ }
29
+ export declare class MetadataService {
30
+ private readonly client;
31
+ private readonly cache;
32
+ constructor(client: ArcheryClient, cache: MetadataCache);
33
+ listInstances(options?: CacheOptions): Promise<CachedPayload>;
34
+ listDatabases(instanceId: number, options?: CacheOptions): Promise<CachedPayload>;
35
+ refreshMetadataCache(options?: RefreshOptions): Promise<RefreshPayload>;
36
+ private wrapRealtime;
37
+ }
38
+ export {};
@@ -0,0 +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)}
@@ -0,0 +1 @@
1
+ export {};
package/dist/smoke.js ADDED
@@ -0,0 +1 @@
1
+ import{ArcheryClient as t}from"./archery-client.js";import{loadConfig as c}from"./config.js";async function e(){const n=c(),o=new t(n),s=await o.ping();console.log(JSON.stringify(s,null,2));const i=await o.listInstances();console.log(JSON.stringify(i,null,2))}e().catch(n=>{const o=n instanceof Error?n.message:String(n);console.error(o),process.exit(1)});
@@ -0,0 +1,2 @@
1
+ export declare function assertReadOnlySql(sql: string): void;
2
+ export declare function normalizeLimit(inputLimit: number | undefined, defaultLimit: number, maxLimit: number): number;
@@ -0,0 +1 @@
1
+ import{ArcheryMcpError as o}from"./errors.js";const i=["select","show","desc","describe","explain","with"],a=["insert","update","delete","drop","alter","truncate","create","replace","grant","revoke","merge","call","execute"];export function assertReadOnlySql(t){const e=c(t).trim().toLowerCase();if(!e)throw new o("ARCHERY_QUERY_REJECTED","SQL \u4E0D\u80FD\u4E3A\u7A7A");if(s(e))throw new o("ARCHERY_QUERY_REJECTED","MCP \u4FA7\u62D2\u7EDD\u591A\u8BED\u53E5 SQL");const r=e.match(/^[a-z]+/)?.[0]??"";if(!i.includes(r))throw new o("ARCHERY_QUERY_REJECTED",`MCP \u4FA7\u53EA\u5141\u8BB8\u53EA\u8BFB SQL\uFF0C\u5F53\u524D\u8BED\u53E5\u4EE5 ${r||"\u672A\u77E5\u5185\u5BB9"} \u5F00\u5934`);for(const n of a)if(new RegExp(`\\b${n}\\b`,"i").test(e))throw new o("ARCHERY_QUERY_REJECTED",`MCP \u4FA7\u62D2\u7EDD\u5305\u542B ${n.toUpperCase()} \u7684 SQL`)}export function normalizeLimit(t,e,r){if(t===void 0||Number.isNaN(t))return e;const n=Math.floor(t);return n<=0?e:Math.min(n,r)}function c(t){let e=t.trimStart(),r="";for(;e!==r;)r=e,e=e.replace(/^--.*(?:\n|$)/,"").trimStart(),e=e.replace(/^\/\*[\s\S]*?\*\//,"").trimStart();return e}function s(t){return t.replace(/;\s*$/,"").includes(";")}
@@ -0,0 +1,5 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { ArcheryClient } from "./archery-client.js";
3
+ import type { ArcheryMcpConfig } from "./config.js";
4
+ import type { MetadataService } from "./metadata-service.js";
5
+ export declare function registerTools(server: Server, client: ArcheryClient, config: ArcheryMcpConfig, metadataService: MetadataService): void;
package/dist/tools.js ADDED
@@ -0,0 +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}:{}}}
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@seaxlab/archery-mcp",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Archery MCP server (Node.js, stdio)",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "archery-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*.js",
12
+ "dist/**/*.d.ts",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.json",
17
+ "build:release": "node scripts/clean-dist.mjs && tsc -p tsconfig.json && node scripts/minify-dist.mjs",
18
+ "prepublishOnly": "npm run build:release",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit",
20
+ "start": "node dist/index.js",
21
+ "smoke": "node dist/smoke.js",
22
+ "dev": "tsx watch src/index.ts",
23
+ "dev:once": "tsx src/index.ts",
24
+ "inspector": "bash scripts/mcp-inspector.sh"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.25.0",
28
+ "zod": "^3.25.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.0.0",
32
+ "esbuild": "^0.25.12",
33
+ "tsx": "^4.19.0",
34
+ "typescript": "^5.8.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }