touchque-mcp-server 1.0.2
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 +128 -0
- package/bundled/sdk-documentation.md +179 -0
- package/bundled/types.ts +582 -0
- package/index.js +495 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# TouchQue MCP Server
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that gives AI coding
|
|
4
|
+
assistants (Claude, Cursor, …) accurate, up-to-date TouchQue SDK
|
|
5
|
+
documentation, TypeScript types, and an integration validator — so they stop
|
|
6
|
+
guessing at the API and stop generating the old, broken synchronous
|
|
7
|
+
integration pattern.
|
|
8
|
+
|
|
9
|
+
It's a small, local, **read-only** tool: it doesn't hold an API key, doesn't
|
|
10
|
+
call your TouchQue account, and doesn't need network access at all for its
|
|
11
|
+
docs/types tools (they're bundled with the package). The only network calls
|
|
12
|
+
it makes are ones you explicitly ask for (`ping_touchque_api`, pointed at a
|
|
13
|
+
URL you pass in).
|
|
14
|
+
|
|
15
|
+
## What it gives an assistant
|
|
16
|
+
|
|
17
|
+
**Tools:**
|
|
18
|
+
- `get_touchque_docs` — the current `@touchque/node` README (install, the
|
|
19
|
+
`requireTouchQue` step-up model, framework adapters, error handling,
|
|
20
|
+
security).
|
|
21
|
+
- `get_sdk_types` — the SDK's TypeScript type definitions (`Config`,
|
|
22
|
+
`Step`/`StepState`, `StartOptions`, `CompleteExpectations`, resource
|
|
23
|
+
response types).
|
|
24
|
+
- `ping_touchque_api` — checks that a TouchQue backend URL you give it is
|
|
25
|
+
reachable, before the assistant blames the integration code for a network
|
|
26
|
+
problem.
|
|
27
|
+
- `validate_integration` — a static-analysis checklist on a pasted route
|
|
28
|
+
handler: is `@touchque/node` actually imported, is the route guarded with
|
|
29
|
+
the current `requireTouchQue`/`withTouchQue`/`touchqueRouter` API (as
|
|
30
|
+
opposed to the old, removed synchronous `tq.login.verify()` pattern), are
|
|
31
|
+
secrets kept out of source and read from the environment, is there error
|
|
32
|
+
handling.
|
|
33
|
+
|
|
34
|
+
**Prompts** (pre-built instruction sets an assistant can load):
|
|
35
|
+
- `integrate_touchque` — a step-by-step wizard for adding TouchQue to an
|
|
36
|
+
existing codebase without breaking anything.
|
|
37
|
+
- `audit_touchque_integration` — a formal security-review checklist for an
|
|
38
|
+
existing integration.
|
|
39
|
+
- `troubleshoot_touchque` — a triage decision tree for a broken integration.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
### Claude Desktop / Claude Code
|
|
44
|
+
|
|
45
|
+
Add to your MCP config (`claude_desktop_config.json`, or `.mcp.json` for
|
|
46
|
+
Claude Code):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"touchque": {
|
|
52
|
+
"command": "npx",
|
|
53
|
+
"args": ["-y", "touchque-mcp-server"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Cursor
|
|
60
|
+
|
|
61
|
+
`~/.cursor/mcp.json` (or the project's `.cursor/mcp.json`):
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"touchque": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "touchque-mcp-server"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
No API key, no environment variables, no separate server process to run —
|
|
75
|
+
`npx` fetches and runs it on demand over stdio, the same way you'd wire up
|
|
76
|
+
any other local MCP server.
|
|
77
|
+
|
|
78
|
+
### Run it directly
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx -y touchque-mcp-server
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It talks [MCP](https://modelcontextprotocol.io) over stdio (stdin/stdout) —
|
|
85
|
+
you won't see anything if you run it directly in a terminal and start typing;
|
|
86
|
+
it's meant to be launched by an MCP-aware client, not used interactively.
|
|
87
|
+
|
|
88
|
+
## Local development
|
|
89
|
+
|
|
90
|
+
If you're editing this server (not consuming it), point it at a docs site
|
|
91
|
+
you're actively editing instead of the bundled copies:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
DOCS_BASE_URL=http://localhost:5176/docs npx touchque-mcp-server
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The server tries `DOCS_BASE_URL/sdk-documentation.md` and
|
|
98
|
+
`DOCS_BASE_URL/types.ts` first when that variable is set, and falls back to
|
|
99
|
+
the bundled copies under [`bundled/`](./bundled) if that fetch fails — so an
|
|
100
|
+
unset or unreachable override never breaks the tool. Keep the bundled copies
|
|
101
|
+
in sync with `sdks/touchque-node/README.md` and
|
|
102
|
+
`sdks/touchque-node/src/{types.ts,steps.ts}` when the SDK's public API
|
|
103
|
+
changes.
|
|
104
|
+
|
|
105
|
+
## Testing
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm test
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Tests spawn the actual server over stdio using the official MCP SDK's
|
|
112
|
+
`Client`/`StdioClientTransport` — the same path any real MCP client takes —
|
|
113
|
+
and check the real tool/prompt list, that the bundled docs/types load without
|
|
114
|
+
network access, and that `validate_integration` correctly distinguishes the
|
|
115
|
+
current step-up API from the old, removed synchronous one.
|
|
116
|
+
|
|
117
|
+
## Security
|
|
118
|
+
|
|
119
|
+
This server is read-only dev tooling: it never touches a real TouchQue
|
|
120
|
+
account, never sees an API key, and never calls a TouchQue backend except a
|
|
121
|
+
URL you explicitly pass to `ping_touchque_api`. There is intentionally no
|
|
122
|
+
authentication on the server itself — it needs none. If that ever changes
|
|
123
|
+
(e.g. a future tool that reads real account data), auth must be added before
|
|
124
|
+
that tool ships.
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT © [TouchQue](https://touchque.com)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# @touchque/node
|
|
2
|
+
|
|
3
|
+
The official Node.js server SDK for [TouchQue](https://touchque.com) — biometric push 2FA,
|
|
4
|
+
passkeys, and offline approval codes, added to any backend with one line per route.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/@touchque/node)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
📘 Full docs: **[authenticator.touchque.com/docs](https://authenticator.touchque.com/docs)**
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @touchque/node
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Get an API key and secret from your [TouchQue Dashboard](https://authenticator.touchque.com),
|
|
21
|
+
then set them as environment variables:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
TQ_API_KEY=tq_auth_your_key
|
|
25
|
+
TQ_API_SECRET=your_api_secret
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The client picks these up automatically — `new TouchQue()` with no arguments.
|
|
29
|
+
|
|
30
|
+
## Quick start (Express)
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import express from 'express';
|
|
34
|
+
import { requireTouchQue } from '@touchque/node';
|
|
35
|
+
|
|
36
|
+
const app = express();
|
|
37
|
+
app.use(express.json());
|
|
38
|
+
|
|
39
|
+
app.post('/transfer',
|
|
40
|
+
requireTouchQue('SEND_MONEY', {
|
|
41
|
+
details: (req) => ({ Amount: `${req.body.amount} EUR`, To: req.body.iban }),
|
|
42
|
+
}),
|
|
43
|
+
(req, res) => {
|
|
44
|
+
// Only reached once the user approved on their phone.
|
|
45
|
+
res.json({ ok: true, assurance: req.touchque.assurance });
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
app.listen(3000);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That's the whole integration for one route. What actually happens:
|
|
53
|
+
|
|
54
|
+
1. The first request comes in with no pending approval → the middleware calls
|
|
55
|
+
TouchQue, sends a push to the user's phone, and answers **`202 { touchque, token }`**
|
|
56
|
+
instead of running your handler.
|
|
57
|
+
2. Your frontend shows `touchque` in its own UI — a matching number for the
|
|
58
|
+
user to tap on their phone, or a QR code the first time they link the app —
|
|
59
|
+
then sends the *same request* again with header `X-TouchQue-Token: <token>`
|
|
60
|
+
(see [`@touchque/web`](https://www.npmjs.com/package/@touchque/web), which does this loop for you).
|
|
61
|
+
3. Once the user approves, that retried request reaches your handler exactly
|
|
62
|
+
once, with `req.touchque` populated (`assurance`, `approvalProof`, …).
|
|
63
|
+
|
|
64
|
+
No hosted page, no redirect, no new domain — you keep your own UI end to end.
|
|
65
|
+
|
|
66
|
+
## The three primitives, if you don't use a framework adapter
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { TouchQue } from '@touchque/node';
|
|
70
|
+
|
|
71
|
+
const tq = new TouchQue(); // from TQ_API_KEY / TQ_API_SECRET
|
|
72
|
+
|
|
73
|
+
const step = await tq.start('SEND_MONEY', {
|
|
74
|
+
user: 'jane@acme.com',
|
|
75
|
+
details: { Amount: '250 EUR', To: 'DE89...' },
|
|
76
|
+
});
|
|
77
|
+
// step.state: 'waiting' (show step.number) | 'enroll' (show step.enroll.qrCodeDataUrl)
|
|
78
|
+
// | 'approved' | 'rejected' | 'expired' | 'passkey_required' | 'frozen' | 'blocked'
|
|
79
|
+
|
|
80
|
+
const latest = await tq.check(step.id);
|
|
81
|
+
|
|
82
|
+
// Once approved, consume it exactly once, right before doing the protected thing:
|
|
83
|
+
const approval = await tq.complete(step.id, {
|
|
84
|
+
user: 'jane@acme.com',
|
|
85
|
+
action: 'SEND_MONEY',
|
|
86
|
+
details: { Amount: '250 EUR', To: 'DE89...' },
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`complete()` verifies the approval was actually issued for this user, action
|
|
91
|
+
and transaction — it will not let an approval for a different amount or
|
|
92
|
+
recipient be replayed against this call, and consuming it twice fails on the
|
|
93
|
+
second call.
|
|
94
|
+
|
|
95
|
+
## Framework adapters
|
|
96
|
+
|
|
97
|
+
- **Express**: `requireTouchQue(action, options?)` — shown above.
|
|
98
|
+
- **Next.js / Fetch API route handlers**: `withTouchQue(action, options?)`.
|
|
99
|
+
- **`touchqueRouter`**: mounts every relay route a frontend needs
|
|
100
|
+
(`POST /login`, enrollment, passkey ceremonies, offline codes) so you don't
|
|
101
|
+
hand-write them.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { touchqueRouter } from '@touchque/node';
|
|
105
|
+
|
|
106
|
+
app.use(touchqueRouter(tq, {
|
|
107
|
+
getUserId: (req) => req.session.user?.email,
|
|
108
|
+
}));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Passkeys (phishing-resistant)
|
|
112
|
+
|
|
113
|
+
Push approval and offline codes stop password reuse and push fatigue, but a
|
|
114
|
+
real-time phishing proxy can still relay them. A passkey can't be phished: the
|
|
115
|
+
browser signs your site's real origin and TouchQue refuses any other
|
|
116
|
+
(NIST SP 800-63B-4 §3.2.5).
|
|
117
|
+
|
|
118
|
+
1. Set your passkey domain in the Dashboard (Security Policy → Passkeys).
|
|
119
|
+
2. Let users register one via `tq.webauthn` (server) + `@touchque/web`'s
|
|
120
|
+
`passkeys.register()` (browser).
|
|
121
|
+
3. Optionally require it for critical actions or every sign-in — TouchQue then
|
|
122
|
+
skips the push and your route resolves `passkey_required` instead.
|
|
123
|
+
4. Check `assurance.phishingResistant` before treating a session as high-assurance.
|
|
124
|
+
|
|
125
|
+
## Offline approval (no internet on the phone)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const ch = await tq.offline.challenge({
|
|
129
|
+
user: 'jane@acme.com',
|
|
130
|
+
type: 'WITHDRAW',
|
|
131
|
+
details: { Amount: '1,250.00 USD', Recipient: 'Jane Doe' },
|
|
132
|
+
});
|
|
133
|
+
// show ch.qrDataUrl — the phone scans it offline and shows a 7-character code
|
|
134
|
+
|
|
135
|
+
const { approved } = await tq.offline.verify({ challengeId: ch.challengeId, code });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Webhooks
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
app.post('/webhooks/touchque', express.raw({ type: 'application/json' }), (req, res) => {
|
|
142
|
+
try {
|
|
143
|
+
const event = tq.webhook.verify({
|
|
144
|
+
rawBody: req.body.toString(),
|
|
145
|
+
signature: req.headers['x-touchque-signature'] as string,
|
|
146
|
+
});
|
|
147
|
+
// handle event.event: 'login.confirmed' | 'login.rejected' | …
|
|
148
|
+
res.sendStatus(200);
|
|
149
|
+
} catch {
|
|
150
|
+
res.sendStatus(403); // not from TouchQue
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Errors
|
|
156
|
+
|
|
157
|
+
All SDK errors extend `TouchQueError`: `TouchQueAPIError`, `TouchQueNetworkError`,
|
|
158
|
+
`TouchQueRejectedError`, `TouchQueTimeoutError`, `TouchQueWebhookSignatureError`,
|
|
159
|
+
`TouchQueConfigError`, `TouchQuePasskeyRequiredError`.
|
|
160
|
+
|
|
161
|
+
## Security
|
|
162
|
+
|
|
163
|
+
- Every API request is signed HMAC-SHA256 (method, path+query, timestamp, nonce, body hash).
|
|
164
|
+
- The `X-TouchQue-Token` a frontend echoes back is itself signed and bound to
|
|
165
|
+
one user + action + transaction digest — it can't be replayed for a
|
|
166
|
+
different amount, recipient or user.
|
|
167
|
+
- An approval is consumed exactly once, server-side.
|
|
168
|
+
- Your API secret never leaves your server.
|
|
169
|
+
|
|
170
|
+
See [SECURITY.md](./SECURITY.md) to report a vulnerability.
|
|
171
|
+
|
|
172
|
+
## Requirements
|
|
173
|
+
|
|
174
|
+
- Node.js 18+
|
|
175
|
+
- A [TouchQue Dashboard](https://authenticator.touchque.com) account
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT © [TouchQue](https://touchque.com)
|