perfume-api 0.0.0-stage → 2.5.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 +21 -0
- package/README.md +178 -2
- package/dist/index.d.ts +72 -0
- package/dist/index.js +122 -0
- package/dist/types.d.ts +334 -0
- package/dist/types.js +2 -0
- package/package.json +59 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 10ML
|
|
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
CHANGED
|
@@ -1,3 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# perfume-api: free perfume API client (10ML Fragrance Index)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Free perfume API client: perfumes, note pyramids, accords, similar scents, notes and houses from the 10ML Fragrance Index, curated by the fragrance editors of [10ML](https://10ml.com). No API key, no dependencies, typed for TypeScript.
|
|
4
|
+
|
|
5
|
+
[](https://registry.modelcontextprotocol.io/v0/servers?search=com.10ml/perfume-finder)
|
|
6
|
+
[](https://www.npmjs.com/package/perfume-api)
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install perfume-api
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Needs Node.js 18 or newer (it uses the global `fetch`).
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { searchPerfumes, perfumeAlternatives, explainNote, comparePerfumes } from 'perfume-api';
|
|
20
|
+
|
|
21
|
+
const found = await searchPerfumes({ q: 'fresh citrus summer cologne for men', limit: 3 });
|
|
22
|
+
console.log(found.results[0].name, found.results[0].accords);
|
|
23
|
+
|
|
24
|
+
const alts = await perfumeAlternatives({ perfume: 'Baccarat Rouge 540' });
|
|
25
|
+
console.log(alts.alternatives.map((a) => `${a.name} (${a.closeness})`));
|
|
26
|
+
|
|
27
|
+
const note = await explainNote('vanilla');
|
|
28
|
+
if (note.found) console.log(note.note.smells_like);
|
|
29
|
+
|
|
30
|
+
const cmp = await comparePerfumes(['Delina', 'Layton']);
|
|
31
|
+
console.log(cmp.shared_notes, cmp.only);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Functions
|
|
35
|
+
|
|
36
|
+
| Function | REST route | What it does |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `searchPerfumes(params)` | `GET /perfumes` | Search by free text, house, notes, accords, family, audience, season, occasion or style |
|
|
39
|
+
| `getPerfume(id, params)` | `GET /perfumes/{id}` | Full profile of one perfume: note pyramid, accords, seasons, similar scents |
|
|
40
|
+
| `similarPerfumes(params)` | `GET /similar` | Perfumes that smell like one you name, or like a list of notes and accords |
|
|
41
|
+
| `perfumeAlternatives(params)` | `GET /alternatives` | The editors' alternatives to a famous perfume, from other houses |
|
|
42
|
+
| `explainNote(id, params)` | `GET /notes/{id}` | What a note smells like, what it pairs with, where it leads |
|
|
43
|
+
| `getHouse(id, params)` | `GET /houses/{id}` | A perfume house: country, style, signature perfumes |
|
|
44
|
+
| `listHouses(params)` | `GET /houses` | Browse houses by type, country or name |
|
|
45
|
+
| `searchGuides(params)` | `GET /guides` | 10ML perfume guides and articles to read or cite |
|
|
46
|
+
| `comparePerfumes(names)` | `GET /perfumes/{id} x 2-4` | Shared and distinct notes and accords of 2 to 4 perfumes (built from their profiles; the REST API has no compare route) |
|
|
47
|
+
|
|
48
|
+
## What comes back
|
|
49
|
+
|
|
50
|
+
Plain JSON, as the REST API sends it (shown trimmed to the main fields). For example `GET /alternatives` and `GET /notes/vanilla`:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
curl -s "https://mcp.10ml.com/api/v1/alternatives?perfume=Baccarat+Rouge+540&limit=2"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"total": 6,
|
|
59
|
+
"original": {
|
|
60
|
+
"name": "Maison Francis Kurkdjian Baccarat Rouge 540",
|
|
61
|
+
"id": "maison-francis-kurkdjian-baccarat-rouge-540-edp"
|
|
62
|
+
},
|
|
63
|
+
"alternatives": [
|
|
64
|
+
{
|
|
65
|
+
"name": "Maison Alhambra Baroque Rouge Extrait",
|
|
66
|
+
"closeness": "very close",
|
|
67
|
+
"why": "Sugared amber, saffron and airy woods aim at the same luminous effect.",
|
|
68
|
+
"id": "maison-alhambra-baroque-rouge-extrait",
|
|
69
|
+
"house": "Maison Alhambra",
|
|
70
|
+
"source": "editors"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "Dossier Ambery Saffron",
|
|
74
|
+
"closeness": "similar vibe",
|
|
75
|
+
"why": "Saffron and amber with sweet woods share the glowing amber idea.",
|
|
76
|
+
"id": "dossier-ambery-saffron",
|
|
77
|
+
"house": "Dossier",
|
|
78
|
+
"source": "editors"
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"source": "10ML Fragrance Index"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
curl -s "https://mcp.10ml.com/api/v1/notes/vanilla"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"found": true,
|
|
92
|
+
"note": {
|
|
93
|
+
"id": "vanilla",
|
|
94
|
+
"name": "vanilla",
|
|
95
|
+
"family": "gourmand",
|
|
96
|
+
"smells_like": "Warm, creamy, sweet and comforting, like custard and cured pods.",
|
|
97
|
+
"mood": "comforting, sweet, warm",
|
|
98
|
+
"pairs_with": [
|
|
99
|
+
"tonka bean",
|
|
100
|
+
"amber",
|
|
101
|
+
"sandalwood",
|
|
102
|
+
"tobacco",
|
|
103
|
+
"caramel",
|
|
104
|
+
"patchouli"
|
|
105
|
+
]
|
|
106
|
+
},
|
|
107
|
+
"source": "10ML Fragrance Index"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Errors
|
|
112
|
+
|
|
113
|
+
An unknown id or name is not an error: the call returns the body with `found: false` and suggestions. A bad request (HTTP 400) or any other failure throws `PerfumeApiError` with `status`, `code`, `message` and the parsed `body`.
|
|
114
|
+
|
|
115
|
+
## Configure
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { createClient } from 'perfume-api';
|
|
119
|
+
|
|
120
|
+
const api = createClient({
|
|
121
|
+
baseUrl: 'https://mcp.10ml.com/api/v1', // the default
|
|
122
|
+
timeoutMs: 15000,
|
|
123
|
+
headers: { 'user-agent': 'my-app/1.0' },
|
|
124
|
+
// fetch: myFetch, // optional: your own fetch
|
|
125
|
+
});
|
|
126
|
+
const houses = await api.listHouses({ type: 'niche', limit: 5 });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Parameters and response fields use the API's own names (`notes_include`, `key_notes`, `next_cursor`). List parameters such as `notes_include` take a string or an array. Responses are fully typed (`SearchPerfumesResponse`, `Perfume`, `Pyramid`, ...), generated from the OpenAPI file.
|
|
130
|
+
|
|
131
|
+
## Example answers
|
|
132
|
+
|
|
133
|
+
**"What is a good alternative to Baccarat Rouge 540 from another house?"** (`find_perfume_alternatives`)
|
|
134
|
+
Maison Alhambra Baroque Rouge Extrait (Maison Alhambra), very close: Sugared amber, saffron and airy woods aim at the same luminous effect.
|
|
135
|
+
|
|
136
|
+
**"A fresh citrus summer cologne for men."** (`search_perfumes`)
|
|
137
|
+
Ciao Citrus (Paris Corner): Grapefruit and bergamot over orange blossom and lily of the valley, drying to musk, sandalwood and amber. Also: Citrus Batikanga (Maison Crivelli): Bitter orange, kalamansi and bergamot are pressed fresh, then a pinch of chili and a vetiver-cedar base keep it lively.
|
|
138
|
+
|
|
139
|
+
**"What does vanilla smell like?"** (`explain_note`)
|
|
140
|
+
vanilla, a gourmand note: Warm, creamy, sweet and comforting, like custard and cured pods. Pairs with tonka bean, amber, sandalwood, tobacco.
|
|
141
|
+
|
|
142
|
+
## Languages
|
|
143
|
+
|
|
144
|
+
Every call takes `locale` (`en`, `de`, `fr`, `es`, `it`, `ro`, `el`, `hu`, `cs`, `pl`) for the 10ml.com links in the answer, and `format` (`concise` or `detailed`). Notes are understood in other languages too: "Vanille" finds vanilla.
|
|
145
|
+
|
|
146
|
+
## Limits and fair use
|
|
147
|
+
|
|
148
|
+
Free and read-only, with no key or sign-in. There is no published rate limit: please cache answers, page long lists with `limit` and `cursor`, and send a descriptive User-Agent from automated clients. For heavy use write to contact@10ml.com.
|
|
149
|
+
|
|
150
|
+
## How it compares
|
|
151
|
+
|
|
152
|
+
In our 40-question benchmark of free, no-key perfume MCP servers (20 October 2026): 39 of 40 correct, median 232 ms. [Read the benchmark](https://10ml.com/perfume-mcp-servers-compared).
|
|
153
|
+
|
|
154
|
+
## FAQ
|
|
155
|
+
|
|
156
|
+
**Is there a free perfume API?**
|
|
157
|
+
Yes. This client talks to the 10ML Fragrance Index API at https://mcp.10ml.com/api/v1: JSON over HTTPS, OpenAPI 3.1 at https://mcp.10ml.com/openapi.json, no API key and no sign-up.
|
|
158
|
+
|
|
159
|
+
**Does it have dependencies?**
|
|
160
|
+
No. The npm package uses the built-in `fetch` (Node 18+), the PyPI package uses `urllib` (Python 3.9+).
|
|
161
|
+
|
|
162
|
+
**Is there a fragrance notes API?**
|
|
163
|
+
Yes: `explainNote` / `explain_note` describes a note (what it smells like, family, origin, pairings), and `searchPerfumes` with `notes_include` lists perfumes built around notes.
|
|
164
|
+
|
|
165
|
+
**Can an AI assistant use it directly?**
|
|
166
|
+
Yes, through the 10ML Perfume Finder MCP server (`https://mcp.10ml.com/mcp`, or `npx -y perfume-mcp` / `uvx perfume-mcp`).
|
|
167
|
+
|
|
168
|
+
**What do I get for a perfume?**
|
|
169
|
+
House, launch year, audience, concentration, family, accords, the note pyramid (top, heart, base), seasons and occasions, an editorial character line and links to 10ml.com guides.
|
|
170
|
+
|
|
171
|
+
## Cite as
|
|
172
|
+
|
|
173
|
+
Cite the answers as "the 10ML Fragrance Index", curated by the fragrance editors of [10ML](https://10ml.com).
|
|
174
|
+
|
|
175
|
+
[mcp.10ml.com](https://mcp.10ml.com) · [10ml.com/perfume-api](https://10ml.com/perfume-api) · [Privacy](https://mcp.10ml.com/privacy) · [Terms](https://mcp.10ml.com/terms) · contact@10ml.com
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* perfume-api: a small typed client for the free 10ML Fragrance Index REST API (https://mcp.10ml.com/api/v1).
|
|
3
|
+
* No API key, no dependencies: it uses the global `fetch` (Node 18+, Deno, Bun, browsers).
|
|
4
|
+
* Parameter and response field names are exactly the API's (snake_case); see https://mcp.10ml.com/openapi.json.
|
|
5
|
+
*/
|
|
6
|
+
import type { FindPerfumeAlternativesParams, FindPerfumeAlternativesResponse, FindSimilarPerfumesParams, FindSimilarPerfumesResponse, GetHouseParams, GetHouseResponse, GetPerfumeParams, GetPerfumeResponse, ExplainNoteParams, ExplainNoteResponse, ListHousesParams, ListHousesResponse, Perfume, SearchGuidesParams, SearchGuidesResponse, SearchPerfumesParams, SearchPerfumesResponse } from './types.js';
|
|
7
|
+
export * from './types.js';
|
|
8
|
+
export declare const DEFAULT_BASE_URL = "https://mcp.10ml.com/api/v1";
|
|
9
|
+
export interface ClientOptions {
|
|
10
|
+
/** API root, default https://mcp.10ml.com/api/v1 */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Your own fetch (tests, proxies, older runtimes). Default: the global fetch. */
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
/** Abort a request after this many milliseconds. Default 15000; 0 disables. */
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
/** Extra request headers. */
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
/** Thrown for every non-2xx answer except 404 (a 404 body is returned: it carries `found: false` and suggestions). */
|
|
20
|
+
export declare class PerfumeApiError extends Error {
|
|
21
|
+
readonly status: number;
|
|
22
|
+
readonly code: string;
|
|
23
|
+
readonly body: unknown;
|
|
24
|
+
constructor(status: number, code: string, message: string, body: unknown);
|
|
25
|
+
}
|
|
26
|
+
/** How two or more perfumes differ; built from their profiles, since the REST API has no compare route. */
|
|
27
|
+
export interface PerfumeComparison {
|
|
28
|
+
perfumes: Perfume[];
|
|
29
|
+
/** Names that are not in the index. */
|
|
30
|
+
not_found: string[];
|
|
31
|
+
same_family: boolean;
|
|
32
|
+
shared_notes: string[];
|
|
33
|
+
shared_accords: string[];
|
|
34
|
+
/** Per perfume id: the notes and accords no other compared perfume has. */
|
|
35
|
+
only: Record<string, {
|
|
36
|
+
notes: string[];
|
|
37
|
+
accords: string[];
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export declare function createClient(options?: ClientOptions): {
|
|
41
|
+
/** Search perfumes by free text, house, notes, accords, family, audience, season, occasion or style. */
|
|
42
|
+
searchPerfumes: (params?: SearchPerfumesParams) => Promise<SearchPerfumesResponse>;
|
|
43
|
+
/** The full profile of one perfume, by name ("Baccarat Rouge 540") or id. Unknown names return `found: false` with suggestions. */
|
|
44
|
+
getPerfume: (id: string, params?: GetPerfumeParams) => Promise<GetPerfumeResponse>;
|
|
45
|
+
/** Perfumes that smell like one you name, or like a list of notes and accords. */
|
|
46
|
+
similarPerfumes: (params: FindSimilarPerfumesParams) => Promise<FindSimilarPerfumesResponse>;
|
|
47
|
+
/** The editors' alternatives to a famous perfume, from other houses. */
|
|
48
|
+
perfumeAlternatives: (params: FindPerfumeAlternativesParams) => Promise<FindPerfumeAlternativesResponse>;
|
|
49
|
+
/** What a fragrance note smells like, what it pairs with and where it leads. */
|
|
50
|
+
explainNote: (id: string, params?: ExplainNoteParams) => Promise<ExplainNoteResponse>;
|
|
51
|
+
/** A perfume house: country, style, what it is known for, signature perfumes. */
|
|
52
|
+
getHouse: (id: string, params?: GetHouseParams) => Promise<GetHouseResponse>;
|
|
53
|
+
/** Browse perfume houses by type, country or name fragment. */
|
|
54
|
+
listHouses: (params?: ListHousesParams) => Promise<ListHousesResponse>;
|
|
55
|
+
/** 10ML perfume guides and articles to read or cite. */
|
|
56
|
+
searchGuides: (params?: SearchGuidesParams) => Promise<SearchGuidesResponse>;
|
|
57
|
+
/**
|
|
58
|
+
* Compare 2 to 4 perfumes. The REST API has no compare route, so this fetches each profile with getPerfume
|
|
59
|
+
* and diffs notes and accords locally.
|
|
60
|
+
*/
|
|
61
|
+
comparePerfumes(names: string[], params?: GetPerfumeParams): Promise<PerfumeComparison>;
|
|
62
|
+
};
|
|
63
|
+
export type PerfumeApiClient = ReturnType<typeof createClient>;
|
|
64
|
+
export declare const searchPerfumes: (params?: SearchPerfumesParams) => Promise<SearchPerfumesResponse>;
|
|
65
|
+
export declare const getPerfume: (id: string, params?: GetPerfumeParams) => Promise<GetPerfumeResponse>;
|
|
66
|
+
export declare const similarPerfumes: (params: FindSimilarPerfumesParams) => Promise<FindSimilarPerfumesResponse>;
|
|
67
|
+
export declare const perfumeAlternatives: (params: FindPerfumeAlternativesParams) => Promise<FindPerfumeAlternativesResponse>;
|
|
68
|
+
export declare const explainNote: (id: string, params?: ExplainNoteParams) => Promise<ExplainNoteResponse>;
|
|
69
|
+
export declare const getHouse: (id: string, params?: GetHouseParams) => Promise<GetHouseResponse>;
|
|
70
|
+
export declare const listHouses: (params?: ListHousesParams) => Promise<ListHousesResponse>;
|
|
71
|
+
export declare const searchGuides: (params?: SearchGuidesParams) => Promise<SearchGuidesResponse>;
|
|
72
|
+
export declare const comparePerfumes: (names: string[], params?: GetPerfumeParams) => Promise<PerfumeComparison>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export const DEFAULT_BASE_URL = 'https://mcp.10ml.com/api/v1';
|
|
3
|
+
/** Thrown for every non-2xx answer except 404 (a 404 body is returned: it carries `found: false` and suggestions). */
|
|
4
|
+
export class PerfumeApiError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
code;
|
|
7
|
+
body;
|
|
8
|
+
constructor(status, code, message, body) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'PerfumeApiError';
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.body = body;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function notesOf(p) {
|
|
17
|
+
const set = new Set(p.key_notes.map((n) => n.toLowerCase()));
|
|
18
|
+
if (p.pyramid)
|
|
19
|
+
for (const n of [...p.pyramid.top, ...p.pyramid.heart, ...p.pyramid.base])
|
|
20
|
+
set.add(n.toLowerCase());
|
|
21
|
+
return [...set];
|
|
22
|
+
}
|
|
23
|
+
export function createClient(options = {}) {
|
|
24
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
25
|
+
const timeoutMs = options.timeoutMs ?? 15000;
|
|
26
|
+
async function get(path, query = {}) {
|
|
27
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
28
|
+
if (typeof doFetch !== 'function')
|
|
29
|
+
throw new Error('perfume-api needs a global fetch (Node 18+) or options.fetch');
|
|
30
|
+
const params = new URLSearchParams();
|
|
31
|
+
for (const [key, value] of Object.entries(query)) {
|
|
32
|
+
if (value === undefined || value === null)
|
|
33
|
+
continue;
|
|
34
|
+
params.set(key, Array.isArray(value) ? value.join(',') : String(value));
|
|
35
|
+
}
|
|
36
|
+
const qs = params.toString();
|
|
37
|
+
const controller = timeoutMs > 0 ? new AbortController() : undefined;
|
|
38
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
|
|
39
|
+
try {
|
|
40
|
+
const res = await doFetch(`${baseUrl}${path}${qs ? `?${qs}` : ''}`, {
|
|
41
|
+
headers: { accept: 'application/json', ...options.headers },
|
|
42
|
+
signal: controller?.signal,
|
|
43
|
+
});
|
|
44
|
+
const text = await res.text();
|
|
45
|
+
let body;
|
|
46
|
+
try {
|
|
47
|
+
body = text ? JSON.parse(text) : undefined;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
body = text;
|
|
51
|
+
}
|
|
52
|
+
if (res.ok || res.status === 404)
|
|
53
|
+
return body;
|
|
54
|
+
const err = body?.error;
|
|
55
|
+
throw new PerfumeApiError(res.status, err?.code ?? 'http_error', err?.message ?? `HTTP ${res.status}`, body);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (timer)
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const seg = (id) => encodeURIComponent(id);
|
|
63
|
+
const api = {
|
|
64
|
+
/** Search perfumes by free text, house, notes, accords, family, audience, season, occasion or style. */
|
|
65
|
+
searchPerfumes: (params = {}) => get('/perfumes', params),
|
|
66
|
+
/** The full profile of one perfume, by name ("Baccarat Rouge 540") or id. Unknown names return `found: false` with suggestions. */
|
|
67
|
+
getPerfume: (id, params = {}) => get(`/perfumes/${seg(id)}`, params),
|
|
68
|
+
/** Perfumes that smell like one you name, or like a list of notes and accords. */
|
|
69
|
+
similarPerfumes: (params) => get('/similar', params),
|
|
70
|
+
/** The editors' alternatives to a famous perfume, from other houses. */
|
|
71
|
+
perfumeAlternatives: (params) => get('/alternatives', params),
|
|
72
|
+
/** What a fragrance note smells like, what it pairs with and where it leads. */
|
|
73
|
+
explainNote: (id, params = {}) => get(`/notes/${seg(id)}`, params),
|
|
74
|
+
/** A perfume house: country, style, what it is known for, signature perfumes. */
|
|
75
|
+
getHouse: (id, params = {}) => get(`/houses/${seg(id)}`, params),
|
|
76
|
+
/** Browse perfume houses by type, country or name fragment. */
|
|
77
|
+
listHouses: (params = {}) => get('/houses', params),
|
|
78
|
+
/** 10ML perfume guides and articles to read or cite. */
|
|
79
|
+
searchGuides: (params = {}) => get('/guides', params),
|
|
80
|
+
/**
|
|
81
|
+
* Compare 2 to 4 perfumes. The REST API has no compare route, so this fetches each profile with getPerfume
|
|
82
|
+
* and diffs notes and accords locally.
|
|
83
|
+
*/
|
|
84
|
+
async comparePerfumes(names, params = {}) {
|
|
85
|
+
if (names.length < 2 || names.length > 4)
|
|
86
|
+
throw new RangeError('comparePerfumes takes 2 to 4 perfumes');
|
|
87
|
+
const answers = await Promise.all(names.map((name) => api.getPerfume(name, params)));
|
|
88
|
+
const perfumes = [];
|
|
89
|
+
const not_found = [];
|
|
90
|
+
answers.forEach((a, i) => (a.found && a.perfume ? perfumes.push(a.perfume) : not_found.push(names[i])));
|
|
91
|
+
const notes = perfumes.map(notesOf);
|
|
92
|
+
const accords = perfumes.map((p) => p.accords.map((a) => a.toLowerCase()));
|
|
93
|
+
const inAll = (lists) => (lists[0] ?? []).filter((x) => lists.length > 0 && lists.every((l) => l.includes(x)));
|
|
94
|
+
const only = {};
|
|
95
|
+
perfumes.forEach((p, i) => {
|
|
96
|
+
const others = (lists) => new Set(lists.filter((_, j) => j !== i).flat());
|
|
97
|
+
const on = others(notes);
|
|
98
|
+
const oa = others(accords);
|
|
99
|
+
only[p.id] = { notes: notes[i].filter((n) => !on.has(n)), accords: accords[i].filter((a) => !oa.has(a)) };
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
perfumes,
|
|
103
|
+
not_found,
|
|
104
|
+
same_family: perfumes.length > 1 && perfumes.every((p) => p.family === perfumes[0].family),
|
|
105
|
+
shared_notes: perfumes.length > 1 ? inAll(notes) : [],
|
|
106
|
+
shared_accords: perfumes.length > 1 ? inAll(accords) : [],
|
|
107
|
+
only,
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return api;
|
|
112
|
+
}
|
|
113
|
+
const defaultClient = createClient();
|
|
114
|
+
export const searchPerfumes = defaultClient.searchPerfumes;
|
|
115
|
+
export const getPerfume = defaultClient.getPerfume;
|
|
116
|
+
export const similarPerfumes = defaultClient.similarPerfumes;
|
|
117
|
+
export const perfumeAlternatives = defaultClient.perfumeAlternatives;
|
|
118
|
+
export const explainNote = defaultClient.explainNote;
|
|
119
|
+
export const getHouse = defaultClient.getHouse;
|
|
120
|
+
export const listHouses = defaultClient.listHouses;
|
|
121
|
+
export const searchGuides = defaultClient.searchGuides;
|
|
122
|
+
export const comparePerfumes = defaultClient.comparePerfumes;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
export interface Perfume {
|
|
2
|
+
/** Stable Fragrance Index id, usable with get_perfume and find_similar_perfumes */
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
house: string;
|
|
6
|
+
/** Launch year when known */
|
|
7
|
+
year?: number;
|
|
8
|
+
gender: "men" | "women" | "unisex";
|
|
9
|
+
/** Fragrance family, e.g. "amber floral" */
|
|
10
|
+
family: string;
|
|
11
|
+
/** Main accords, strongest first */
|
|
12
|
+
accords: string[];
|
|
13
|
+
/** The defining notes (empty when no pyramid is published) */
|
|
14
|
+
key_notes: string[];
|
|
15
|
+
/** One-sentence description by 10ML's fragrance editors */
|
|
16
|
+
character: string;
|
|
17
|
+
/** 1-3 relevant 10ml.com links */
|
|
18
|
+
links: Link[];
|
|
19
|
+
profile: "full" | "core";
|
|
20
|
+
concentration?: string;
|
|
21
|
+
pyramid?: Pyramid;
|
|
22
|
+
seasons?: string[];
|
|
23
|
+
occasions?: string[];
|
|
24
|
+
styles?: string[];
|
|
25
|
+
/** 1 (short) to 5 (very long) */
|
|
26
|
+
longevity?: number;
|
|
27
|
+
/** 1 (skin scent) to 5 (fills a room) */
|
|
28
|
+
projection?: number;
|
|
29
|
+
perfumers?: string[];
|
|
30
|
+
/** Ids of other concentrations or flankers of the same perfume in the index */
|
|
31
|
+
other_versions?: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface Link {
|
|
34
|
+
type: "try" | "guide" | "note";
|
|
35
|
+
title: string;
|
|
36
|
+
/** A 10ml.com link in the requested locale */
|
|
37
|
+
url: string;
|
|
38
|
+
}
|
|
39
|
+
export interface Pyramid {
|
|
40
|
+
top: string[];
|
|
41
|
+
heart: string[];
|
|
42
|
+
base: string[];
|
|
43
|
+
}
|
|
44
|
+
export interface SearchPerfumesResponse {
|
|
45
|
+
/** How many results matched in all */
|
|
46
|
+
total: number;
|
|
47
|
+
/** Pass as cursor to get the next page; absent on the last page */
|
|
48
|
+
next_cursor?: string;
|
|
49
|
+
/** Guidance when the result is empty, partial or needs a follow-up tool */
|
|
50
|
+
message?: string;
|
|
51
|
+
results: Perfume[];
|
|
52
|
+
/** The filters applied, including ones read from the query text */
|
|
53
|
+
filters: Record<string, string>;
|
|
54
|
+
/** True when no perfume matched every word and the closest partial matches are shown */
|
|
55
|
+
relaxed: boolean;
|
|
56
|
+
source: string;
|
|
57
|
+
}
|
|
58
|
+
export interface SearchPerfumesParams {
|
|
59
|
+
/** Free text: a perfume name, house, notes or mood. Examples: "fresh citrus summer for men", "Tom Ford oud", "vanilla gourmand for winter" */
|
|
60
|
+
q?: string;
|
|
61
|
+
/** Only perfumes from this house. Examples: "Dior", "Maison Francis Kurkdjian", "MFK", "YSL" */
|
|
62
|
+
house?: string;
|
|
63
|
+
/** Every listed note must be present. Examples: ["oud","sandalwood"], ["vanilla"] Comma-separated list. */
|
|
64
|
+
notes_include?: string | string[];
|
|
65
|
+
/** None of these notes may be present. Example: ["patchouli"] Comma-separated list. */
|
|
66
|
+
notes_exclude?: string | string[];
|
|
67
|
+
/** Accords that must be present. Examples: ["woody","sweet"], ["fresh","aquatic"] Comma-separated list. */
|
|
68
|
+
accords?: string | string[];
|
|
69
|
+
/** Fragrance family. Examples: "chypre", "fougere", "gourmand", "amber", "citrus". Leave term empty in explain_perfume_term to list every family. */
|
|
70
|
+
family?: string;
|
|
71
|
+
/** Audience the perfume is marketed to. men and women also include unisex perfumes. */
|
|
72
|
+
gender?: "men" | "women" | "unisex";
|
|
73
|
+
/** Season the perfume suits (fall is accepted in text queries as autumn). */
|
|
74
|
+
season?: "spring" | "summer" | "autumn" | "winter";
|
|
75
|
+
/** Occasion the perfume suits. */
|
|
76
|
+
occasion?: "daytime" | "office" | "casual" | "evening" | "date night" | "special occasion";
|
|
77
|
+
/** A style word the scent should have. */
|
|
78
|
+
style?: "aquatic" | "boozy" | "clean" | "creamy" | "dark" | "floral" | "fresh" | "citrusy" | "fruity" | "gourmand" | "green" | "leathery" | "musky" | "powdery" | "sensual" | "smoky" | "spicy" | "sweet" | "woody";
|
|
79
|
+
/** true = only the editors' iconic, era-defining classics (works with every other filter, and without a query) */
|
|
80
|
+
iconic?: boolean;
|
|
81
|
+
/** Results per page. */
|
|
82
|
+
limit?: number;
|
|
83
|
+
/** next_cursor from the previous page. */
|
|
84
|
+
cursor?: string;
|
|
85
|
+
/** concise (default) or detailed (adds pyramid, seasons, occasions, longevity). */
|
|
86
|
+
format?: "concise" | "detailed";
|
|
87
|
+
/** Language of the 10ml.com links and labels. */
|
|
88
|
+
locale?: "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
89
|
+
}
|
|
90
|
+
export interface GetPerfumeResponse {
|
|
91
|
+
found: boolean;
|
|
92
|
+
/** False when this is a closest-match guess */
|
|
93
|
+
exact: boolean;
|
|
94
|
+
perfume?: Perfume;
|
|
95
|
+
house_info?: {
|
|
96
|
+
name: string;
|
|
97
|
+
country?: string;
|
|
98
|
+
type: string;
|
|
99
|
+
style: string;
|
|
100
|
+
};
|
|
101
|
+
similar?: Array<{
|
|
102
|
+
id: string;
|
|
103
|
+
name: string;
|
|
104
|
+
house: string;
|
|
105
|
+
closeness: "very close" | "similar vibe" | "same spirit";
|
|
106
|
+
shared_notes: string[];
|
|
107
|
+
url: string;
|
|
108
|
+
}>;
|
|
109
|
+
alternatives?: Array<{
|
|
110
|
+
name: string;
|
|
111
|
+
house?: string;
|
|
112
|
+
closeness: "very close" | "similar vibe" | "same spirit";
|
|
113
|
+
why: string;
|
|
114
|
+
id?: string;
|
|
115
|
+
}>;
|
|
116
|
+
guides?: Link[];
|
|
117
|
+
suggestions?: Perfume[];
|
|
118
|
+
message?: string;
|
|
119
|
+
source: string;
|
|
120
|
+
}
|
|
121
|
+
export interface GetPerfumeParams {
|
|
122
|
+
/** House, when the name alone is ambiguous. Example: "Creed" */
|
|
123
|
+
house?: string;
|
|
124
|
+
/** concise (default) or detailed (adds pyramid, seasons, occasions, longevity). */
|
|
125
|
+
format?: "concise" | "detailed";
|
|
126
|
+
/** Language of the 10ml.com links and labels. */
|
|
127
|
+
locale?: "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
128
|
+
}
|
|
129
|
+
export interface FindSimilarPerfumesResponse {
|
|
130
|
+
/** How many results matched in all */
|
|
131
|
+
total: number;
|
|
132
|
+
/** Pass as cursor to get the next page; absent on the last page */
|
|
133
|
+
next_cursor?: string;
|
|
134
|
+
/** Guidance when the result is empty, partial or needs a follow-up tool */
|
|
135
|
+
message?: string;
|
|
136
|
+
target: {
|
|
137
|
+
name: string;
|
|
138
|
+
house: string | null;
|
|
139
|
+
in_index: boolean;
|
|
140
|
+
id?: string;
|
|
141
|
+
notes: string[];
|
|
142
|
+
accords: string[];
|
|
143
|
+
};
|
|
144
|
+
results: Array<{
|
|
145
|
+
perfume: Perfume;
|
|
146
|
+
closeness: "very close" | "similar vibe" | "same spirit";
|
|
147
|
+
shared_notes: string[];
|
|
148
|
+
shared_accords: string[];
|
|
149
|
+
why: string;
|
|
150
|
+
}>;
|
|
151
|
+
/** Notes that are not in the index vocabulary and were ignored */
|
|
152
|
+
unrecognised_notes?: string[];
|
|
153
|
+
source: string;
|
|
154
|
+
}
|
|
155
|
+
export interface FindSimilarPerfumesParams {
|
|
156
|
+
/** A perfume by name, optionally with its house, e.g. "Baccarat Rouge 540", "Dior Sauvage", "Delina". Or pass an id from another tool. */
|
|
157
|
+
perfume?: string;
|
|
158
|
+
/** House of the perfume when the name alone is ambiguous, e.g. "Maison Francis Kurkdjian" */
|
|
159
|
+
house?: string;
|
|
160
|
+
/** Known notes of the starting perfume, for perfumes outside the index. Example: ["jasmine","saffron","ambergris","cedar"] Comma-separated list. */
|
|
161
|
+
notes?: string | string[];
|
|
162
|
+
/** Known accords of the starting perfume. Example: ["amber","woody","sweet"] Comma-separated list. */
|
|
163
|
+
accords?: string | string[];
|
|
164
|
+
/** Move away from the starting point in one direction: fresher, sweeter, darker, lighter, longer_lasting (needs a perfume in the index) or more_projection (same). */
|
|
165
|
+
steer?: "fresher" | "sweeter" | "darker" | "lighter" | "longer_lasting" | "more_projection";
|
|
166
|
+
/** Only return perfumes for this audience (men and women include unisex) */
|
|
167
|
+
gender?: "men" | "women" | "unisex";
|
|
168
|
+
/** True to leave out perfumes from the same house as the starting perfume */
|
|
169
|
+
other_houses_only?: boolean;
|
|
170
|
+
/** Results per page. */
|
|
171
|
+
limit?: number;
|
|
172
|
+
/** next_cursor from the previous page. */
|
|
173
|
+
cursor?: string;
|
|
174
|
+
/** concise (default) or detailed (adds pyramid, seasons, occasions, longevity). */
|
|
175
|
+
format?: "concise" | "detailed";
|
|
176
|
+
/** Language of the 10ml.com links and labels. */
|
|
177
|
+
locale?: "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
178
|
+
}
|
|
179
|
+
export interface FindPerfumeAlternativesResponse {
|
|
180
|
+
/** How many results matched in all */
|
|
181
|
+
total: number;
|
|
182
|
+
/** Pass as cursor to get the next page; absent on the last page */
|
|
183
|
+
next_cursor?: string;
|
|
184
|
+
/** Guidance when the result is empty, partial or needs a follow-up tool */
|
|
185
|
+
message?: string;
|
|
186
|
+
original: {
|
|
187
|
+
name: string;
|
|
188
|
+
house?: string;
|
|
189
|
+
id?: string;
|
|
190
|
+
};
|
|
191
|
+
alternatives: Array<{
|
|
192
|
+
name: string;
|
|
193
|
+
house?: string;
|
|
194
|
+
closeness: "very close" | "similar vibe" | "same spirit";
|
|
195
|
+
why: string;
|
|
196
|
+
/** Fragrance Index id when the alternative has its own profile */
|
|
197
|
+
id?: string;
|
|
198
|
+
source: "editors" | "similarity";
|
|
199
|
+
}>;
|
|
200
|
+
/** The 10ML guide comparing this perfume with its alternatives */
|
|
201
|
+
guide?: Link;
|
|
202
|
+
links: Link[];
|
|
203
|
+
/** Other originals the query could also mean */
|
|
204
|
+
other_matches?: string[];
|
|
205
|
+
source: string;
|
|
206
|
+
}
|
|
207
|
+
export interface FindPerfumeAlternativesParams {
|
|
208
|
+
/** The famous perfume to find alternatives for. Examples: "Creed Aventus", "Baccarat Rouge 540", "Delina" */
|
|
209
|
+
perfume?: string;
|
|
210
|
+
/** House of the original when the name is ambiguous, e.g. "Parfums de Marly" */
|
|
211
|
+
house?: string;
|
|
212
|
+
/** Only alternatives at this closeness */
|
|
213
|
+
closeness?: "very close" | "similar vibe" | "same spirit";
|
|
214
|
+
/** Results per page. */
|
|
215
|
+
limit?: number;
|
|
216
|
+
/** next_cursor from the previous page. */
|
|
217
|
+
cursor?: string;
|
|
218
|
+
/** Language of the 10ml.com links and labels. */
|
|
219
|
+
locale?: "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
220
|
+
}
|
|
221
|
+
export interface ExplainNoteResponse {
|
|
222
|
+
found: boolean;
|
|
223
|
+
note?: {
|
|
224
|
+
id: string;
|
|
225
|
+
name: string;
|
|
226
|
+
family: string;
|
|
227
|
+
origin: string;
|
|
228
|
+
smells_like: string;
|
|
229
|
+
mood: string;
|
|
230
|
+
description?: string;
|
|
231
|
+
also_called: string[];
|
|
232
|
+
pairs_with: string[];
|
|
233
|
+
similar_notes: string[];
|
|
234
|
+
};
|
|
235
|
+
famous_perfumes?: Perfume[];
|
|
236
|
+
links?: Link[];
|
|
237
|
+
suggestions?: string[];
|
|
238
|
+
message?: string;
|
|
239
|
+
source: string;
|
|
240
|
+
}
|
|
241
|
+
export interface ExplainNoteParams {
|
|
242
|
+
/** concise (default) or detailed (adds pyramid, seasons, occasions, longevity). */
|
|
243
|
+
format?: "concise" | "detailed";
|
|
244
|
+
/** Language of the 10ml.com links and labels. */
|
|
245
|
+
locale?: "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
246
|
+
}
|
|
247
|
+
export interface ListHousesResponse {
|
|
248
|
+
/** How many results matched in all */
|
|
249
|
+
total: number;
|
|
250
|
+
/** Pass as cursor to get the next page; absent on the last page */
|
|
251
|
+
next_cursor?: string;
|
|
252
|
+
/** Guidance when the result is empty, partial or needs a follow-up tool */
|
|
253
|
+
message?: string;
|
|
254
|
+
houses: Array<{
|
|
255
|
+
id: string;
|
|
256
|
+
name: string;
|
|
257
|
+
country?: string;
|
|
258
|
+
type: string;
|
|
259
|
+
founded?: number;
|
|
260
|
+
perfumes_in_index: number;
|
|
261
|
+
known_for: string[];
|
|
262
|
+
}>;
|
|
263
|
+
source: string;
|
|
264
|
+
}
|
|
265
|
+
export interface ListHousesParams {
|
|
266
|
+
/** Only houses of this type */
|
|
267
|
+
type?: "designer" | "niche" | "middle-eastern" | "indie" | "celebrity";
|
|
268
|
+
/** Only houses from this country, e.g. "France", "Italy", "United Arab Emirates" */
|
|
269
|
+
country?: string;
|
|
270
|
+
/** Part of a house name, e.g. "tom" or "maison" */
|
|
271
|
+
query?: string;
|
|
272
|
+
/** Results per page. */
|
|
273
|
+
limit?: number;
|
|
274
|
+
/** next_cursor from the previous page. */
|
|
275
|
+
cursor?: string;
|
|
276
|
+
/** Language of the 10ml.com links and labels. */
|
|
277
|
+
locale?: "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
278
|
+
}
|
|
279
|
+
export interface GetHouseResponse {
|
|
280
|
+
found: boolean;
|
|
281
|
+
house?: {
|
|
282
|
+
id: string;
|
|
283
|
+
name: string;
|
|
284
|
+
country?: string;
|
|
285
|
+
type: string;
|
|
286
|
+
founded?: number;
|
|
287
|
+
perfumes_in_index: number;
|
|
288
|
+
known_for: string[];
|
|
289
|
+
style: string;
|
|
290
|
+
aliases: string[];
|
|
291
|
+
};
|
|
292
|
+
signature_perfumes?: Perfume[];
|
|
293
|
+
links?: Link[];
|
|
294
|
+
suggestions?: string[];
|
|
295
|
+
message?: string;
|
|
296
|
+
source: string;
|
|
297
|
+
}
|
|
298
|
+
export interface GetHouseParams {
|
|
299
|
+
/** concise (default) or detailed (adds pyramid, seasons, occasions, longevity). */
|
|
300
|
+
format?: "concise" | "detailed";
|
|
301
|
+
/** Language of the 10ml.com links and labels. */
|
|
302
|
+
locale?: "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
303
|
+
}
|
|
304
|
+
export interface SearchGuidesResponse {
|
|
305
|
+
/** How many results matched in all */
|
|
306
|
+
total: number;
|
|
307
|
+
/** Pass as cursor to get the next page; absent on the last page */
|
|
308
|
+
next_cursor?: string;
|
|
309
|
+
/** Guidance when the result is empty, partial or needs a follow-up tool */
|
|
310
|
+
message?: string;
|
|
311
|
+
guides: Array<{
|
|
312
|
+
title: string;
|
|
313
|
+
type: string;
|
|
314
|
+
topics: string[];
|
|
315
|
+
url?: string;
|
|
316
|
+
/** False when the guide is only available in English (the url is then the English page) */
|
|
317
|
+
translated: boolean;
|
|
318
|
+
}>;
|
|
319
|
+
source: string;
|
|
320
|
+
}
|
|
321
|
+
export interface SearchGuidesParams {
|
|
322
|
+
/** What the guide is about. Examples: "how to make perfume last", "oud", "Creed Aventus dupes", "gift". Omit to browse. */
|
|
323
|
+
q?: string;
|
|
324
|
+
/** how-to, list (rankings/comparisons), alternatives (dupes guides), note (a note explained), house (a house mapped) */
|
|
325
|
+
type?: "how-to" | "list" | "alternatives" | "note" | "house";
|
|
326
|
+
/** Results per page. */
|
|
327
|
+
limit?: number;
|
|
328
|
+
/** next_cursor from the previous page. */
|
|
329
|
+
cursor?: string;
|
|
330
|
+
/** Language of the 10ml.com links and labels. */
|
|
331
|
+
locale?: "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
332
|
+
}
|
|
333
|
+
export type Locale = "en" | "de" | "fr" | "es" | "it" | "ro" | "el" | "hu" | "cs" | "pl";
|
|
334
|
+
export type ResponseFormat = "concise" | "detailed";
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perfume-api",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "Free perfume API client: perfumes, note pyramids, accords, similar scents, notes and houses from the 10ML Fragrance Index. No API key, JSON over HTTPS.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"perfume",
|
|
7
|
+
"fragrance",
|
|
8
|
+
"cologne",
|
|
9
|
+
"perfume-api",
|
|
10
|
+
"fragrance-api",
|
|
11
|
+
"perfume-mcp",
|
|
12
|
+
"fragrance-mcp",
|
|
13
|
+
"mcp",
|
|
14
|
+
"mcp-server",
|
|
15
|
+
"model-context-protocol",
|
|
16
|
+
"perfume-notes",
|
|
17
|
+
"fragrance-notes",
|
|
18
|
+
"scent",
|
|
19
|
+
"eau-de-parfum",
|
|
20
|
+
"perfume-recommendation",
|
|
21
|
+
"similar-perfumes",
|
|
22
|
+
"claude",
|
|
23
|
+
"chatgpt",
|
|
24
|
+
"cursor"
|
|
25
|
+
],
|
|
26
|
+
"homepage": "https://mcp.10ml.com",
|
|
27
|
+
"author": "10ML Team <contact@10ml.com> (https://10ml.com)",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "dist/index.js",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"gen": "node ../scripts/gen-types.mjs",
|
|
52
|
+
"build": "tsc",
|
|
53
|
+
"test": "npm run build && node --test test/client.test.mjs",
|
|
54
|
+
"smoke": "node test/smoke.mjs",
|
|
55
|
+
"prepublishOnly": "npm run build"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^22.20.5",
|
|
59
|
+
"typescript": "^5.9.0"
|
|
60
|
+
}
|
|
61
|
+
}
|