@lynq/lynq 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 hogekai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # lynq
2
+
3
+ [![CI](https://github.com/hogekai/lynq/actions/workflows/ci.yml/badge.svg)](https://github.com/hogekai/lynq/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/lynq)](https://www.npmjs.com/package/lynq)
5
+
6
+ Lightweight MCP server framework. Tool visibility control through middleware.
7
+
8
+ ```ts
9
+ import { createMCPServer } from "lynq";
10
+ import { auth } from "lynq/auth";
11
+ import { z } from "zod";
12
+
13
+ const server = createMCPServer({ name: "my-server", version: "1.0.0" });
14
+
15
+ // Login tool — always visible
16
+ server.tool("login", {
17
+ input: z.object({ username: z.string(), password: z.string() }),
18
+ }, async (args, ctx) => {
19
+ const user = await authenticate(args.username, args.password);
20
+ ctx.session.set("user", user);
21
+ ctx.session.authorize("auth");
22
+ return { content: [{ type: "text", text: `Welcome, ${user.name}` }] };
23
+ });
24
+
25
+ // Weather tool — hidden until authenticated
26
+ server.tool("weather", auth(), {
27
+ description: "Get weather for a city",
28
+ input: z.object({ city: z.string() }),
29
+ }, async (args) => {
30
+ const data = await fetchWeather(args.city);
31
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
32
+ });
33
+
34
+ await server.stdio();
35
+ ```
36
+
37
+ ## Install
38
+
39
+ ```sh
40
+ npm install lynq @modelcontextprotocol/sdk zod
41
+ ```
42
+
43
+ ## Why lynq
44
+
45
+ When you build an MCP server, you often need to show different tools depending on session state — hide admin tools from unauthenticated users, reveal features after onboarding, etc. The MCP protocol supports this via bidirectional tool list notifications, but wiring it by hand means managing visibility sets, diffing tool lists, and calling `sendToolListChanged` at the right time. lynq lets you declare visibility as middleware and handles the rest.
46
+
47
+ ## API
48
+
49
+ ### `createMCPServer(info)`
50
+
51
+ Creates a server instance.
52
+
53
+ ```ts
54
+ const server = createMCPServer({ name: "my-server", version: "1.0.0" });
55
+ ```
56
+
57
+ ### `server.tool(name, ...middlewares?, config, handler)`
58
+
59
+ Register a tool. Middlewares are optional, config holds `description` and `input` schema.
60
+
61
+ ```ts
62
+ server.tool("greet", {
63
+ description: "Greet someone",
64
+ input: z.object({ name: z.string() }),
65
+ }, async (args) => ({
66
+ content: [{ type: "text", text: `Hello ${args.name}` }],
67
+ }));
68
+
69
+ server.tool("secret", auth(), {
70
+ input: z.object({ query: z.string() }),
71
+ }, async (args) => ({
72
+ content: [{ type: "text", text: args.query }],
73
+ }));
74
+ ```
75
+
76
+ ### `server.resource(uri, ...middlewares?, config, handler)`
77
+
78
+ Register a resource. Same middleware pattern as `tool()`. Global middleware (`server.use()`) does not apply to resources.
79
+
80
+ ```ts
81
+ server.resource("config://settings", {
82
+ name: "App Settings",
83
+ mimeType: "application/json",
84
+ }, async (uri) => ({
85
+ text: JSON.stringify(config),
86
+ }));
87
+
88
+ server.resource("data://users", auth(), {
89
+ name: "User Database",
90
+ mimeType: "application/json",
91
+ }, async (uri, ctx) => ({
92
+ text: JSON.stringify(await db.getUsers()),
93
+ }));
94
+ ```
95
+
96
+ ### `server.use(middleware)`
97
+
98
+ Apply middleware to all subsequently registered tools.
99
+
100
+ ```ts
101
+ server.use(auth());
102
+ ```
103
+
104
+ ### Session
105
+
106
+ Available in handlers and middleware via `ctx.session`:
107
+
108
+ ```ts
109
+ ctx.session.set("key", value);
110
+ ctx.session.get("key");
111
+ ctx.session.authorize("auth"); // Enable tools guarded by "auth" middleware
112
+ ```
113
+
114
+ ### `auth(options?)`
115
+
116
+ Middleware that hides tools until authenticated.
117
+
118
+ ```ts
119
+ import { auth } from "lynq/auth";
120
+
121
+ auth(); // checks ctx.session.get("user")
122
+ auth({ sessionKey: "token" }); // checks ctx.session.get("token")
123
+ auth({ message: "Login first" }); // custom error message
124
+ ```
125
+
126
+ ## Testing
127
+
128
+ lynq ships a test helper that eliminates MCP boilerplate. No manual `Client`/`InMemoryTransport` setup.
129
+
130
+ ```ts
131
+ import { createTestClient } from "lynq/test";
132
+ import { createMCPServer } from "lynq";
133
+ import { auth } from "lynq/auth";
134
+ import { z } from "zod";
135
+
136
+ const server = createMCPServer({ name: "my-server", version: "1.0.0" });
137
+ server.tool("weather", auth(), {
138
+ input: z.object({ city: z.string() }),
139
+ }, async (args) => ({
140
+ content: [{ type: "text", text: `Sunny in ${args.city}` }],
141
+ }));
142
+
143
+ const t = await createTestClient(server);
144
+
145
+ // Tool visibility
146
+ const tools = await t.listTools(); // string[]
147
+ expect(tools).not.toContain("weather");
148
+
149
+ // Authorize and call
150
+ t.authorize("auth");
151
+ const text = await t.callToolText("weather", { city: "Tokyo" });
152
+ expect(text).toContain("Sunny");
153
+
154
+ // Full result access
155
+ const result = await t.callTool("weather", { city: "Tokyo" });
156
+
157
+ // Resources
158
+ const uris = await t.listResources();
159
+ const content = await t.readResource("config://settings");
160
+
161
+ // Session access
162
+ t.session.set("user", { name: "alice" });
163
+
164
+ await t.close();
165
+ ```
166
+
167
+ ### Custom matchers
168
+
169
+ Optional vitest/jest matchers for more expressive assertions:
170
+
171
+ ```ts
172
+ import { matchers } from "lynq/test";
173
+ expect.extend(matchers);
174
+
175
+ const result = await t.callTool("weather", { city: "Tokyo" });
176
+ expect(result).toHaveTextContent("Sunny");
177
+ expect(result).not.toBeError();
178
+ ```
179
+
180
+ ## License
181
+
182
+ MIT
@@ -0,0 +1,14 @@
1
+ import { Express } from 'express';
2
+ import { M as MCPServer } from '../types-BqH9Me9B.js';
3
+ import '@modelcontextprotocol/sdk/types.js';
4
+ import 'zod';
5
+
6
+ interface MountOptions {
7
+ /** Route path. Default: "/mcp" */
8
+ path?: string;
9
+ /** Allowed hostnames for DNS rebinding protection. Default: localhost variants. */
10
+ allowedHosts?: string[];
11
+ }
12
+ declare function mountLynq(app: Express, server: MCPServer, options?: MountOptions): void;
13
+
14
+ export { type MountOptions, mountLynq };
@@ -0,0 +1,2 @@
1
+ import {b,a}from'../chunk-7C4A572Z.mjs';function u(e){let t=e.protocol||"http",s=e.headers.host||"localhost",o=`${t}://${s}${e.originalUrl}`,r=new Headers;for(let[a,n]of Object.entries(e.headers))n&&r.set(a,Array.isArray(n)?n.join(", "):n);let i={method:e.method,headers:r};return e.method!=="GET"&&e.method!=="HEAD"&&(i.body=JSON.stringify(e.body)),new Request(o,i)}async function h(e,t){if(e.status(t.status),t.headers.forEach((s,o)=>e.setHeader(o,s)),t.body){let s=t.body.getReader();try{for(;;){let{done:o,value:r}=await s.read();if(o)break;e.write(r);}}finally{e.end();}}else e.end();}function y(e,t,s){let o=s?.path??"/mcp",r=t.http(),i=s?.allowedHosts??[...b];e.use(o,(a$1,n,d)=>{if(!a(a$1.headers.host??null,i)){n.status(403).json({jsonrpc:"2.0",error:{code:-32e3,message:"Forbidden"},id:null});return}d();}),e.all(o,async(a,n)=>{let d=u(a),p=await r(d);await h(n,p);});}
2
+ export{y as mountLynq};
@@ -0,0 +1,14 @@
1
+ import { Hono } from 'hono';
2
+ import { M as MCPServer } from '../types-BqH9Me9B.js';
3
+ import '@modelcontextprotocol/sdk/types.js';
4
+ import 'zod';
5
+
6
+ interface MountOptions {
7
+ /** Route path. Default: "/mcp" */
8
+ path?: string;
9
+ /** Allowed hostnames for DNS rebinding protection. Default: localhost variants. */
10
+ allowedHosts?: string[];
11
+ }
12
+ declare function mountLynq(app: Hono, server: MCPServer, options?: MountOptions): void;
13
+
14
+ export { type MountOptions, mountLynq };
@@ -0,0 +1 @@
1
+ import {b,a}from'../chunk-7C4A572Z.mjs';function m(t,a$1,e){let r=e?.path??"/mcp",l=a$1.http(),i=e?.allowedHosts??[...b];t.use(r,async(o,p)=>a(o.req.header("host")??null,i)?p():o.json({jsonrpc:"2.0",error:{code:-32e3,message:"Forbidden"},id:null},403)),t.all(r,o=>l(o.req.raw));}export{m as mountLynq};
@@ -0,0 +1 @@
1
+ export { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -0,0 +1 @@
1
+ export{StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';
@@ -0,0 +1 @@
1
+ function e(n,t){if(!n)return false;let o=n.replace(/:\d+$/,"");return t.includes(o)}var l=["localhost","127.0.0.1","::1"];export{e as a,l as b};
@@ -0,0 +1,11 @@
1
+ import { M as MCPServer } from './types-BqH9Me9B.js';
2
+ export { E as Elicit, a as ElicitFormParams, b as ElicitFormResult, c as ElicitUrlParams, d as ElicitUrlResult, H as HttpAdapterOptions, R as ResourceConfig, e as ResourceContent, f as ResourceContext, g as ResourceHandler, h as RootInfo, S as Sample, i as SampleOptions, j as SampleRawParams, k as SampleRawResult, l as ServerInfo, m as Session, T as TaskConfig, n as TaskContext, o as TaskControl, p as TaskHandler, q as ToolConfig, r as ToolContext, s as ToolHandler, t as ToolInfo, u as ToolMiddleware } from './types-BqH9Me9B.js';
3
+ import '@modelcontextprotocol/sdk/types.js';
4
+ import 'zod';
5
+
6
+ declare function createMCPServer(info: {
7
+ name: string;
8
+ version: string;
9
+ }): MCPServer;
10
+
11
+ export { MCPServer, createMCPServer };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import {InMemoryTaskStore}from'@modelcontextprotocol/sdk/experimental/tasks';import {Server}from'@modelcontextprotocol/sdk/server/index.js';import {ListToolsRequestSchema,CallToolRequestSchema,ListResourcesRequestSchema,ListResourceTemplatesRequestSchema,ReadResourceRequestSchema}from'@modelcontextprotocol/sdk/types.js';import {normalizeObjectSchema}from'@modelcontextprotocol/sdk/server/zod-compat.js';import {toJsonSchemaCompat}from'@modelcontextprotocol/sdk/server/zod-json-schema-compat.js';function ne(a){return {async form({message:l,schema:c}){let i=await a.elicitInput({message:l,requestedSchema:{type:"object",properties:c}});return {action:i.action,content:i.content??{}}},async url({message:l,url:c}){return {action:(await a.elicitInput({mode:"url",message:l,url:c,elicitationId:crypto.randomUUID()})).action}}}}function A(a){return async()=>{try{return (await a.listRoots()).roots.map(c=>{let i={uri:c.uri};return c.name!==void 0&&(i.name=c.name),i})}catch{return []}}}function se(a){async function l(i,u){let p={messages:[{role:"user",content:{type:"text",text:i}}],maxTokens:u?.maxTokens??1024};u?.model!==void 0&&(p.modelPreferences={hints:[{name:u.model}]}),u?.system!==void 0&&(p.systemPrompt=u.system),u?.temperature!==void 0&&(p.temperature=u.temperature),u?.stopSequences!==void 0&&(p.stopSequences=u.stopSequences);let f=(await a.createMessage(p)).content;return f.type==="text"?f.text:""}async function c(i){return a.createMessage(i)}return Object.assign(l,{raw:c})}function q(a,l,c,i,u){return {toolName:i,session:c,signal:u,sessionId:l,elicit:ne(a),roots:A(a),sample:se(a)}}function B(a){if(a==null)return {type:"object"};let l=normalizeObjectSchema(a);return l?toJsonSchemaCompat(l):a}function H(a,l){let c=l[l.length-1];if(typeof c!="function")throw new TypeError(`${a}: last argument must be a handler function`);let i=l[l.length-2];if(i==null||typeof i!="object"||Array.isArray(i))throw new TypeError(`${a}: second-to-last argument must be a config object`);let u=l.slice(0,-2);for(let p of u)if(!p||typeof p!="object"||typeof p.name!="string")throw new TypeError(`${a}: each middleware must have a "name" property`);return {middlewares:u,config:i,handler:c}}function j(a,l){let c=[];for(let i of l)i.onRegister?.(a)===false&&c.push(i.name);return c}function L(a){let c=a.split(/\{[^}]+\}/).map(i=>i.replace(/[.*+?^$|()[\]\\]/g,"\\$&"));return new RegExp(`^${c.join("(.+)")}$`)}function E(a,l,c,i){let u=c.get(l);if(u==="disabled")return false;if(u==="enabled")return true;for(let p of a)if(!i.has(p))return false;return true}function z(a,l){let c=a.get(l);if(c)return c;for(let i of a.values())if(i.isTemplate&&i.uriPattern?.test(l))return i}function $(a,l,c){let i=a.filter(f=>f.onCall),u=a.filter(f=>f.onResult).reverse(),p=0,y=async()=>{if(p>=i.length){let b=await c();for(let C of u)b=await C.onResult(b,l);return b}return i[p++].onCall(l,y)};return y}function pe(a){let l=[],c=new Map,i=new Map,u=new Map,p=new Map,y=new Map,f=new Set,b=new InMemoryTaskStore,C=new Proxy(b,{get(e,n,t){return n==="updateTaskStatus"?async(s,r,...o)=>(r==="cancelled"&&f.add(s),e.updateTaskStatus.call(e,s,r,...o)):Reflect.get(e,n,t)}}),h=new Server(a,{capabilities:{tools:{listChanged:true},resources:{listChanged:true},tasks:{list:{},cancel:{},requests:{tools:{call:{}}}}},taskStore:C});function S(e){let n=p.get(e);return n||(n={data:new Map,grants:new Set,toolOverrides:new Map,resourceOverrides:new Map},p.set(e,n)),n}function O(e,n){let t=S(n);return E(e.hiddenByMiddlewares,e.name,t.toolOverrides,t.grants)}function k(e,n){let t=S(n);return E(e.hiddenByMiddlewares,e.uri,t.resourceOverrides,t.grants)}function _(e,n){let t=S(n);return E(e.hiddenByMiddlewares,e.name,t.toolOverrides,t.grants)}function M(e){(e&&y.get(e)||h).sendToolListChanged().catch(()=>{});}function x(e){(e&&y.get(e)||h).sendResourceListChanged().catch(()=>{});}function I(e){let n=S(e);return {get(t){return n.data.get(t)},set(t,s){n.data.set(t,s);},authorize(t){n.grants.add(t),M(e),x(e);},revoke(t){n.grants.delete(t),M(e),x(e);},enableTools(...t){for(let s of t)n.toolOverrides.set(s,"enabled");M(e);},disableTools(...t){for(let s of t)n.toolOverrides.set(s,"disabled");M(e);},enableResources(...t){for(let s of t)n.resourceOverrides.set(s,"enabled");x(e);},disableResources(...t){for(let s of t)n.resourceOverrides.set(s,"disabled");x(e);}}}function V(e){e.setRequestHandler(ListToolsRequestSchema,(n,t)=>{let s=t.sessionId??"default",r=[];for(let o of c.values())O(o,s)&&r.push({name:o.name,description:o.description,inputSchema:B(o.input)});for(let o of u.values())_(o,s)&&r.push({name:o.name,description:o.description,inputSchema:B(o.input),execution:{taskSupport:"required"}});return {tools:r}}),e.setRequestHandler(CallToolRequestSchema,async(n,t)=>{let{name:s,arguments:r}=n.params,o=t.sessionId??"default",w=d=>({content:[{type:"text",text:d}],isError:true}),g=c.get(s);if(g){if(!O(g,o))return w(`Tool not available: ${s}`);let d=q(e,o,I(o),s,t.signal),v=()=>Promise.resolve(g.handler(r??{},d));return $(g.middlewares,d,v)()}let T=u.get(s);if(T){if(!_(T,o))return w(`Tool not available: ${s}`);let d=t.taskStore;if(!d)return w("Task store not available");let v=await d.createTask({pollInterval:1e3}),m=v.taskId,Y={progress(R,P){if(f.has(m))return;let te=P?`${R}% ${P}`:`${R}%`;d.updateTaskStatus(m,"working",te).catch(()=>{});},get cancelled(){return f.has(m)}},J={...q(e,o,I(o),s,t.signal),task:Y},ee=async()=>((async()=>{try{let R=await T.handler(r??{},J);f.has(m)||await d.storeTaskResult(m,"completed",R);}catch(R){if(!f.has(m)){let P=R instanceof Error?R.message:String(R);await d.storeTaskResult(m,"failed",{content:[{type:"text",text:P}],isError:true}).catch(()=>{});}}})(),{task:v});return $(T.middlewares,J,ee)()}return w(`Unknown tool: ${s}`)}),e.setRequestHandler(ListResourcesRequestSchema,(n,t)=>{let s=t.sessionId??"default",r=[];for(let o of i.values())!o.isTemplate&&k(o,s)&&r.push({uri:o.uri,name:o.name,description:o.description,mimeType:o.mimeType});return {resources:r}}),e.setRequestHandler(ListResourceTemplatesRequestSchema,(n,t)=>{let s=t.sessionId??"default",r=[];for(let o of i.values())o.isTemplate&&k(o,s)&&r.push({uriTemplate:o.uri,name:o.name,description:o.description,mimeType:o.mimeType});return {resourceTemplates:r}}),e.setRequestHandler(ReadResourceRequestSchema,async(n,t)=>{let{uri:s}=n.params,r=z(i,s);if(!r)throw new Error(`Unknown resource: ${s}`);let o=t.sessionId??"default";if(!k(r,o))throw new Error(`Resource not available: ${s}`);let w=I(o),g={uri:s,session:w,sessionId:o,roots:A(e)},T=q(e,o,w,r.uri,t.signal),d=async()=>{let m=await r.handler(s,g);return {contents:[{uri:s,mimeType:m.mimeType??r.mimeType,...m.text!=null?{text:m.text}:{},...m.blob!=null?{blob:m.blob}:{}}]}};return $(r.middlewares,T,d)()});}V(h);function F(e){l.push(e);}function G(...e){let n=e[0],t=H(`tool("${n}")`,e.slice(1));if(typeof t.config.name=="string")throw new TypeError(`tool("${n}"): second-to-last argument must be a config object`);let s=t.config,r=[...l,...t.middlewares],o={name:n,description:s.description,middlewares:r};c.set(n,{name:n,description:s.description,input:s.input,handler:t.handler,middlewares:r,hiddenByMiddlewares:j(o,r)});}function D(...e){let n=e[0],t=H(`resource("${n}")`,e.slice(1));if(typeof t.config.name!="string")throw new TypeError(`resource("${n}"): second-to-last argument must be a config object with a "name" property`);let s=t.config,r={name:s.name,description:s.description,middlewares:t.middlewares},o=n.includes("{");i.set(n,{uri:n,isTemplate:o,uriPattern:o?L(n):null,name:s.name,description:s.description,mimeType:s.mimeType,handler:t.handler,middlewares:t.middlewares,hiddenByMiddlewares:j(r,t.middlewares)});}function W(...e){let n=e[0],t=H(`task("${n}")`,e.slice(1));if(typeof t.config.name=="string")throw new TypeError(`task("${n}"): second-to-last argument must be a config object`);let s=t.config,r=[...l,...t.middlewares],o={name:n,description:s.description,middlewares:r};u.set(n,{name:n,description:s.description,input:s.input,handler:t.handler,middlewares:r,hiddenByMiddlewares:j(o,r)});}async function Z(){let{StdioServerTransport:e}=await import('@modelcontextprotocol/sdk/server/stdio.js'),n=new e;await h.connect(n);}async function K(e){await h.connect(e);}let Q={tools:{listChanged:true},resources:{listChanged:true},tasks:{list:{},cancel:{},requests:{tools:{call:{}}}}};function U(){let e=new Server(a,{capabilities:Q,taskStore:C});return V(e),e}function X(e){let n=null;async function t(){return n||(n=(await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js')).WebStandardStreamableHTTPServerTransport),n}if(e?.sessionless)return async r=>{let o=await t(),w=U(),g=new o({sessionIdGenerator:void 0,enableJsonResponse:e?.enableJsonResponse});return await w.connect(g),g.handleRequest(r)};let s=new Map;return async r=>{let o=await t(),w=r.headers.get("mcp-session-id");if(w){let d=s.get(w);return d?d.transport.handleRequest(r):new Response(JSON.stringify({jsonrpc:"2.0",error:{code:-32e3,message:"Session not found"}}),{status:404,headers:{"Content-Type":"application/json"}})}let g=U(),T=new o({sessionIdGenerator:e?.sessionIdGenerator??(()=>crypto.randomUUID()),enableJsonResponse:e?.enableJsonResponse,onsessioninitialized:d=>{s.set(d,{server:g,transport:T}),y.set(d,g);},onsessionclosed:d=>{s.delete(d),y.delete(d);}});return await g.connect(T),T.handleRequest(r)}}return {use:F,tool:G,resource:D,task:W,stdio:Z,http:X,connect:K,_server:h,_getSession:S,_isToolVisible(e,n){let t=c.get(e);return t?O(t,n):false},_isResourceVisible(e,n){let t=i.get(e);return t?k(t,n):false},_isTaskVisible(e,n){let t=u.get(e);return t?_(t,n):false},_createSessionAPI:I}}export{pe as createMCPServer};
@@ -0,0 +1,13 @@
1
+ import { u as ToolMiddleware } from '../types-BqH9Me9B.js';
2
+ import '@modelcontextprotocol/sdk/types.js';
3
+ import 'zod';
4
+
5
+ interface AuthOptions {
6
+ /** Session key to check for authentication. Default: "user" */
7
+ sessionKey?: string;
8
+ /** Error message when not authenticated. */
9
+ message?: string;
10
+ }
11
+ declare function auth(options?: AuthOptions): ToolMiddleware;
12
+
13
+ export { type AuthOptions, auth };
@@ -0,0 +1 @@
1
+ function i(e){let t=e?.sessionKey??"user",s=e?.message??"Authentication required. Please login first.";return {name:"auth",onRegister(){return false},async onCall(n,r){return n.session.get(t)?r():{content:[{type:"text",text:s}],isError:true}}}}export{i as auth};
package/dist/test.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import { m as Session, M as MCPServer } from './types-BqH9Me9B.js';
3
+ import 'zod';
4
+
5
+ interface TestClient {
6
+ /** List visible tool names. */
7
+ listTools(): Promise<string[]>;
8
+ /** Call a tool and return the full result. */
9
+ callTool(name: string, args?: Record<string, unknown>): Promise<CallToolResult>;
10
+ /** Call a tool and return the first text content. Throws if isError. */
11
+ callToolText(name: string, args?: Record<string, unknown>): Promise<string>;
12
+ /** List visible resource URIs (static only). */
13
+ listResources(): Promise<string[]>;
14
+ /** List visible resource template URIs. */
15
+ listResourceTemplates(): Promise<string[]>;
16
+ /** Read a resource and return its text content. */
17
+ readResource(uri: string): Promise<string>;
18
+ /** Authorize a middleware (enable tools/resources guarded by it). */
19
+ authorize(middlewareName: string): void;
20
+ /** Revoke a middleware authorization. */
21
+ revoke(middlewareName: string): void;
22
+ /** Direct session access for test setup. */
23
+ session: Session;
24
+ /** Close the test client and clean up. */
25
+ close(): Promise<void>;
26
+ }
27
+ declare function createTestClient(server: MCPServer): Promise<TestClient>;
28
+ declare const matchers: {
29
+ toHaveTextContent(received: CallToolResult, expected: string): {
30
+ pass: boolean;
31
+ message: () => string;
32
+ };
33
+ toBeError(received: CallToolResult): {
34
+ pass: boolean;
35
+ message: () => "Expected result not to be an error" | "Expected result to be an error (isError: true)";
36
+ };
37
+ };
38
+
39
+ export { type TestClient, createTestClient, matchers };
package/dist/test.mjs ADDED
@@ -0,0 +1 @@
1
+ async function d(n){let{Client:r}=await import('@modelcontextprotocol/sdk/client/index.js'),{InMemoryTransport:c}=await import('@modelcontextprotocol/sdk/inMemory.js'),[o,a]=c.createLinkedPair(),t=new r({name:"lynq-test",version:"1.0.0"},{capabilities:{tasks:{}}}),u=n;await Promise.all([u._server.connect(a),t.connect(o)]);let l=u._createSessionAPI("default");return {async listTools(){return (await t.listTools()).tools.map(s=>s.name)},async callTool(e,s={}){return t.callTool({name:e,arguments:s})},async callToolText(e,s={}){let i=await t.callTool({name:e,arguments:s});if(i.isError){let p=i.content.find(g=>g.type==="text")?.text??"Unknown error";throw new Error(p)}return i.content.find(m=>m.type==="text")?.text??""},async listResources(){return (await t.listResources()).resources.map(s=>s.uri)},async listResourceTemplates(){return (await t.listResourceTemplates()).resourceTemplates.map(s=>s.uriTemplate)},async readResource(e){return (await t.readResource({uri:e})).contents[0]?.text??""},authorize(e){l.authorize(e);},revoke(e){l.revoke(e);},session:l,async close(){await t.close();}}}var y={toHaveTextContent(n,r){let o=n.content.filter(t=>t.type==="text").map(t=>t.text??""),a=o.some(t=>t.includes(r));return {pass:a,message:()=>a?`Expected result not to contain "${r}"`:`Expected result to contain "${r}", got: ${o.join(", ")}`}},toBeError(n){let r=n.isError===true;return {pass:r,message:()=>r?"Expected result not to be an error":"Expected result to be an error (isError: true)"}}};export{d as createTestClient,y as matchers};
@@ -0,0 +1,176 @@
1
+ import { CreateMessageRequestParamsBase, CreateMessageResult, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import { z } from 'zod';
3
+
4
+ interface ServerInfo {
5
+ name: string;
6
+ version: string;
7
+ }
8
+ interface Session {
9
+ /** Get a session-scoped value. */
10
+ get<T = unknown>(key: string): T | undefined;
11
+ /** Set a session-scoped value. */
12
+ set(key: string, value: unknown): void;
13
+ /** Authorize a middleware by name, enabling all tools and resources guarded by it. */
14
+ authorize(middlewareName: string): void;
15
+ /** Revoke a middleware authorization, disabling all tools and resources guarded by it. */
16
+ revoke(middlewareName: string): void;
17
+ /** Enable specific tools by name. */
18
+ enableTools(...names: string[]): void;
19
+ /** Disable specific tools by name. */
20
+ disableTools(...names: string[]): void;
21
+ /** Enable specific resources by URI. */
22
+ enableResources(...uris: string[]): void;
23
+ /** Disable specific resources by URI. */
24
+ disableResources(...uris: string[]): void;
25
+ }
26
+ interface ElicitFormParams {
27
+ message: string;
28
+ schema: Record<string, {
29
+ type: "string" | "number" | "boolean";
30
+ description?: string;
31
+ enum?: string[];
32
+ default?: unknown;
33
+ }>;
34
+ }
35
+ interface ElicitFormResult {
36
+ action: "accept" | "decline" | "cancel";
37
+ content: Record<string, string | number | boolean | string[]>;
38
+ }
39
+ interface ElicitUrlParams {
40
+ message: string;
41
+ url: string;
42
+ }
43
+ interface ElicitUrlResult {
44
+ action: "accept" | "decline" | "cancel";
45
+ }
46
+ interface Elicit {
47
+ /** Request structured data from the user via a form. */
48
+ form(params: ElicitFormParams): Promise<ElicitFormResult>;
49
+ /** Direct the user to an external URL. */
50
+ url(params: ElicitUrlParams): Promise<ElicitUrlResult>;
51
+ }
52
+ interface SampleOptions {
53
+ maxTokens?: number;
54
+ /** Model hint — the client makes the final decision. */
55
+ model?: string;
56
+ system?: string;
57
+ temperature?: number;
58
+ stopSequences?: string[];
59
+ }
60
+ type SampleRawParams = CreateMessageRequestParamsBase;
61
+ type SampleRawResult = CreateMessageResult;
62
+ interface Sample {
63
+ /** Send text, get text back. */
64
+ (prompt: string, options?: SampleOptions): Promise<string>;
65
+ /** Full SDK createMessage params and result. */
66
+ raw(params: SampleRawParams): Promise<SampleRawResult>;
67
+ }
68
+ interface RootInfo {
69
+ /** The root URI. Currently always `file://`. */
70
+ uri: string;
71
+ /** Optional human-readable name for the root. */
72
+ name?: string;
73
+ }
74
+ interface ToolContext {
75
+ /** The name of the tool being called. */
76
+ toolName: string;
77
+ /** Session-scoped state and visibility control. */
78
+ session: Session;
79
+ /** Abort signal from the client. */
80
+ signal: AbortSignal;
81
+ /** Session ID. */
82
+ sessionId: string;
83
+ /** Request information from the user. */
84
+ elicit: Elicit;
85
+ /** Query client-provided filesystem roots. */
86
+ roots: () => Promise<RootInfo[]>;
87
+ /** Request LLM inference from the client. */
88
+ sample: Sample;
89
+ }
90
+ interface ToolInfo {
91
+ name: string;
92
+ description?: string | undefined;
93
+ middlewares: readonly ToolMiddleware[];
94
+ }
95
+ interface ToolMiddleware {
96
+ /** Unique name for this middleware instance. Used for authorize()/revoke(). */
97
+ name: string;
98
+ /** Called when a tool is registered. Return false to hide the tool initially. */
99
+ onRegister?(tool: ToolInfo): boolean | undefined;
100
+ /** Called when a tool is invoked. Must call next() to continue the chain. */
101
+ onCall?(ctx: ToolContext, next: () => Promise<CallToolResult>): Promise<CallToolResult>;
102
+ /** Called after the handler returns. Runs in reverse middleware order. */
103
+ onResult?(result: CallToolResult, ctx: ToolContext): CallToolResult | Promise<CallToolResult>;
104
+ }
105
+ type InferInput<T> = T extends z.ZodTypeAny ? z.output<T> : Record<string, unknown>;
106
+ interface ToolConfig<TInput = unknown> {
107
+ description?: string;
108
+ input?: TInput;
109
+ }
110
+ type ToolHandler<TInput = unknown> = (args: InferInput<TInput>, ctx: ToolContext) => CallToolResult | Promise<CallToolResult>;
111
+ /** @experimental */
112
+ interface TaskConfig<TInput = unknown> {
113
+ description?: string;
114
+ input?: TInput;
115
+ }
116
+ /** @experimental */
117
+ interface TaskControl {
118
+ /** Report progress. percentage: 0-100. message: optional status text. */
119
+ progress(percentage: number, message?: string): void;
120
+ /** True when the client has cancelled this task. */
121
+ readonly cancelled: boolean;
122
+ }
123
+ /** @experimental */
124
+ interface TaskContext extends ToolContext {
125
+ task: TaskControl;
126
+ }
127
+ /** @experimental */
128
+ type TaskHandler<TInput = unknown> = (args: InferInput<TInput>, ctx: TaskContext) => CallToolResult | Promise<CallToolResult>;
129
+ interface ResourceConfig {
130
+ name: string;
131
+ description?: string;
132
+ mimeType?: string;
133
+ }
134
+ interface ResourceContent {
135
+ text?: string;
136
+ blob?: string;
137
+ mimeType?: string;
138
+ }
139
+ interface ResourceContext {
140
+ uri: string;
141
+ session: Session;
142
+ sessionId: string;
143
+ /** Query client-provided filesystem roots. */
144
+ roots: () => Promise<RootInfo[]>;
145
+ }
146
+ type ResourceHandler = (uri: string, ctx: ResourceContext) => ResourceContent | Promise<ResourceContent>;
147
+ interface HttpAdapterOptions {
148
+ /** Disable session management. Default: false. */
149
+ sessionless?: boolean;
150
+ /** Custom session ID generator. Default: crypto.randomUUID(). */
151
+ sessionIdGenerator?: () => string;
152
+ /** Return JSON instead of SSE streams. Default: false. */
153
+ enableJsonResponse?: boolean;
154
+ }
155
+ interface MCPServer {
156
+ /** Register a global middleware applied to all subsequently registered tools. */
157
+ use(middleware: ToolMiddleware): void;
158
+ /** Register a tool with config and handler. */
159
+ tool<TInput>(name: string, config: ToolConfig<TInput>, handler: ToolHandler<TInput>): void;
160
+ /** Register a tool with per-tool middlewares, config, and handler. */
161
+ tool<TInput>(name: string, ...args: [...ToolMiddleware[], ToolConfig<TInput>, ToolHandler<TInput>]): void;
162
+ /** Register a resource with config and handler. */
163
+ resource(uri: string, config: ResourceConfig, handler: ResourceHandler): void;
164
+ /** Register a resource with per-resource middlewares, config, and handler. */
165
+ resource(uri: string, ...args: [...ToolMiddleware[], ResourceConfig, ResourceHandler]): void;
166
+ /** @experimental Register a task with config and handler. */
167
+ task<TInput>(name: string, config: TaskConfig<TInput>, handler: TaskHandler<TInput>): void;
168
+ /** @experimental Register a task with per-task middlewares, config, and handler. */
169
+ task<TInput>(name: string, ...args: [...ToolMiddleware[], TaskConfig<TInput>, TaskHandler<TInput>]): void;
170
+ /** Start stdio transport. */
171
+ stdio(): Promise<void>;
172
+ /** Start HTTP transport. Returns a Web Standard request handler. */
173
+ http(options?: HttpAdapterOptions): (req: Request) => Promise<Response>;
174
+ }
175
+
176
+ export type { Elicit as E, HttpAdapterOptions as H, MCPServer as M, ResourceConfig as R, Sample as S, TaskConfig as T, ElicitFormParams as a, ElicitFormResult as b, ElicitUrlParams as c, ElicitUrlResult as d, ResourceContent as e, ResourceContext as f, ResourceHandler as g, RootInfo as h, SampleOptions as i, SampleRawParams as j, SampleRawResult as k, ServerInfo as l, Session as m, TaskContext as n, TaskControl as o, TaskHandler as p, ToolConfig as q, ToolContext as r, ToolHandler as s, ToolInfo as t, ToolMiddleware as u };
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "@lynq/lynq",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight MCP server framework. Tool visibility control through middleware.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.mjs"
10
+ },
11
+ "./auth": {
12
+ "types": "./dist/middleware/auth.d.ts",
13
+ "import": "./dist/middleware/auth.mjs"
14
+ },
15
+ "./stdio": {
16
+ "types": "./dist/adapters/stdio.d.ts",
17
+ "import": "./dist/adapters/stdio.mjs"
18
+ },
19
+ "./hono": {
20
+ "types": "./dist/adapters/hono.d.ts",
21
+ "import": "./dist/adapters/hono.mjs"
22
+ },
23
+ "./express": {
24
+ "types": "./dist/adapters/express.d.ts",
25
+ "import": "./dist/adapters/express.mjs"
26
+ },
27
+ "./test": {
28
+ "types": "./dist/test.d.ts",
29
+ "import": "./dist/test.mjs"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "sideEffects": false,
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "test": "vitest run",
39
+ "test:watch": "vitest",
40
+ "lint": "biome check src tests",
41
+ "lint:fix": "biome check --write src tests",
42
+ "typecheck": "tsc --noEmit",
43
+ "prepublishOnly": "pnpm build"
44
+ },
45
+ "devDependencies": {
46
+ "@biomejs/biome": "^1.9.0",
47
+ "@modelcontextprotocol/sdk": "^1.27.0",
48
+ "@types/express": "^5.0.6",
49
+ "express": "^5.2.1",
50
+ "hono": "^4.12.5",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.6.0",
53
+ "vitest": "^2.0.0",
54
+ "zod": "^3.24.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@modelcontextprotocol/sdk": "^1.27.0",
58
+ "express": "^5.0.0",
59
+ "hono": "^4.0.0",
60
+ "zod": "^3.0.0"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "zod": {
64
+ "optional": true
65
+ },
66
+ "hono": {
67
+ "optional": true
68
+ },
69
+ "express": {
70
+ "optional": true
71
+ }
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ },
76
+ "keywords": [
77
+ "mcp",
78
+ "model-context-protocol",
79
+ "framework",
80
+ "middleware",
81
+ "ai",
82
+ "agent"
83
+ ],
84
+ "repository": {
85
+ "type": "git",
86
+ "url": "https://github.com/hogekai/lynq"
87
+ },
88
+ "bugs": {
89
+ "url": "https://github.com/hogekai/lynq/issues"
90
+ },
91
+ "homepage": "https://github.com/hogekai/lynq#readme",
92
+ "license": "MIT",
93
+ "engines": {
94
+ "node": ">=18"
95
+ },
96
+ "packageManager": "pnpm@10.30.1",
97
+ "pnpm": {
98
+ "onlyBuiltDependencies": [
99
+ "@biomejs/biome",
100
+ "esbuild"
101
+ ]
102
+ }
103
+ }