TEPRA LINK 2 saves labels as .tm2 files. tepra.tm2 writes them.
Type a list once, and get one label per line.
Then print from the app, as usual.
It works from an AI assistant over MCP, from the command line, and from Go.
Important
.tm2 is the format of TEPRA LINK 2 only.
TEPRA Creator (テプラ クリエイター), the app for Windows, does not open these files.
tepra.tm2 does not write files for TEPRA Creator.
Warning
This is an unofficial tool. KING JIM does not document the .tm2 format.
- tepra.tm2 writes every key that files from the app have.
- Each setting was checked on the screen of TEPRA LINK 2 for Android, with an SR-MK1.
- A new release of the app may change the format.
| Way | How |
|---|---|
| mise (recommended) | mise use -g github:mpyw/tepra.tm2 |
| A release binary | Download the archive for your OS and CPU from Releases. Put tm2 on your PATH. checksums.txt has their SHA-256. |
| From source | go install github.com/mpyw/tepra.tm2/cmd/tm2@latest |
Releases cover macOS, Linux and Windows, each on amd64 and arm64.
The MCP server is tm2 mcp.
| Assistant | How |
|---|---|
| Claude Desktop, on macOS or Windows | Download tm2.mcpb and open it. Claude Desktop asks you to install it. It needs nothing else installed |
| Claude Code | First install tm2 as a tool, as above. Then run claude mcp add tm2 -- tm2 mcp |
| Another assistant | First install tm2 as a tool. Add a server whose command is the absolute path of tm2, with the argument mcp |
Important
tm2.mcpb adds the tools to the chat of Claude Desktop only.
The Code tab of Claude Desktop runs Claude Code, and its sessions do not get them.
go get github.com/mpyw/tepra.tm2The package is named tm2.
Ask for labels in your own words.
The assistant writes a .tm2 file with the MCP tools.
It can also read a .tm2 file and change it, one the app saved too.
Then print the file from TEPRA LINK 2.
One label of two lines:
tm2 text -o sample.tm2 $'サンプル\nテキスト'wrote sample.tm2: 1 label, 18.2 mm long, on 12 mm tape
Tip
$'...' turns \n into a line break in bash and zsh.
A line break inside quotes works too. In PowerShell, write "サンプル`nテキスト".
A label per row of a CSV file, each in the size that fits it:
tm2 pages -o pages.tm2 items.csvwrote pages.tm2: 3 pages, 12.0 to 22.5 mm long, on 12 mm tape
Then print the file from TEPRA LINK 2.
f, err := tm2.NewPages(
tm2.Tape12mm,
[]string{"1行目\n2行目", "1行目"},
tm2.WithAlign(tm2.AlignCenter),
)
if err != nil {
return err
}
return f.WriteFile("items.tm2")- All: Kinds of label, Tapes, Layout, Print the file
- MCP: MCP tools
- Tool: Commands, The CSV file, Tool options, tm2 inspect, tm2 edit
- All: Patches
- Library: Functions, Library options, Option types, Errors, Reading files
| text | pages | pour (流し込み) | |
|---|---|---|---|
| Labels in a file | One | One per page | One per value |
| Font size and length | Fitted to the label | Fitted to each label | Same for every label, fitted to the longest |
| Edit in the app | As one label | Page by page | As pour data, in a table |
| QR codes and barcodes | No | No | Yes |
| MCP tool | create_text_label |
create_label_pages |
create_pour_labels |
| Command | tm2 text |
tm2 pages |
tm2 pour |
| Go | NewText |
NewPages |
NewPour |
Tip
For a list of things to label, use pages. Each label gets the size that fits it. Give a length, and every label is as long. Use pour only for QR codes and barcodes, or to edit the labels as one table in the app. Pour lays text out less well: see Layout.
| Width | Go |
|---|---|
| 4, 6, 9, 12, 18, 24, 36 and 50 mm | Tape4mm to Tape50mm, or LookupTape(width) |
The sizes of each tape come from a file the app wrote on it.
Size and length:
- Text fills the height of the tape, unless a font size is given.
- A label is as long as what is on it, and at least 12 mm with its margins. The app makes no shorter label.
- A label of a fixed length has its content centred. Text too long for it gets smaller, unless a font size is given.
- All the labels of a pour file have one font size, the one that fits the value of most lines.
What you see when you edit the label in the app:
- A border is an empty box of its own, around the text.
A border set on a pour drawer itself (
BIType) does not show. - Inverted text of more than one line is a box per line. In a pour file, each line is a column of its own:
Label,Label 2and on.
What pour cannot do:
- The text sits at the left of its frame. The app ignores the alignment of a pour drawer.
- Inverted text is black only behind the letters, not over the whole frame.
- The text keeps its font size, and its width is an estimate. Bold text and wide letters, such as
wandm, can run past the frame.
Pages with a length have none of these limits.
Note
On 4 mm tape, a border's padding of 1.5 mm leaves no room for text. Give a padding of 0.3 mm there.
- Send the
.tm2file to your phone, such as through Google Drive. - Open it with TEPRA LINK 2, through the share menu of the OS.
- Check the labels in the app, and print.
tm2 mcp serves these on stdin and stdout:
| Tool | Kind | What it does |
|---|---|---|
create_text_label |
text | Writes a file of one label |
create_label_pages |
pages | Writes a file of one page per string |
create_pour_labels |
pour | Writes a pour file: one label per string |
inspect_label |
Describes a .tm2 file. With full, it also returns every key as JSON, and each picture |
|
edit_label |
Changes a .tm2 file by a patch |
Inputs:
- The tools that write a file take the tool options, in snake case.
- A length in mm ends in
_mm, and a picture in_path. Examples:font_size,rotate,padding_mm,icon_path. codeandcolumnare forcreate_pour_labelsonly.- Paths must be absolute, for the file and for pictures.
- A file is not replaced unless
overwriteis true.edit_labelchanges the file atpathin place without it. It writes tooutputinstead when given. The label then takes the name ofoutputin the app, unless the patch sets/fileName. - A line break in a label is
\n.
| Command | What it does |
|---|---|
tm2 text [options] text |
Writes one label. A line break in the text breaks it into lines |
tm2 pages [options] [file.csv] |
Writes a page per row of the file |
tm2 pour [options] [file.csv] |
Writes a pour label per row of the file |
tm2 inspect [--json] file.tm2 |
Shows what a .tm2 file holds |
tm2 edit [-o out.tm2] [-f] file.tm2 [patch.json] |
Changes a .tm2 file by a patch |
tm2 mcp |
Serves the MCP tools on stdin and stdout |
A pour file of the same items.csv:
tm2 pour -o items.tm2 items.csvwrote items.tm2: 3 labels, 12.0 mm each, on 12 mm tape
A QR code per URL, one label each:
tm2 pour --code qr -o qr.tm2 urls.csvwrote qr.tm2: 2 labels, 12.2 mm each, on 12 mm tape
A label of a fixed length, with a rounded border:
tm2 text --length 60 --border rounded --align center -o fixed.tm2 サンプルwrote fixed.tm2: 1 label, 60.0 mm long, on 12 mm tape
Text turned 90 degrees, and text in white on black:
tm2 text --rotate 90 -o turned.tm2 $'サンプル\nテキスト'
tm2 text --fill inverted --bold -o inverted.tm2 サンプルwrote turned.tm2: 1 label, 12.0 mm long, on 12 mm tape
wrote inverted.tm2: 1 label, 34.5 mm long, on 12 mm tape
pages and pour read a CSV file of one column.
Without a file, or with -, they read stdin.
- Each row is a label.
- A line break in a value breaks the label into lines.
- A value in double quotes may hold line breaks and commas.
- A double quote in a value is doubled, inside double quotes:
"He said ""hi""". - Blank rows are skipped. So are spaces around a value.
- A row of two or more columns is an error.
- The file must be UTF-8. In Excel, save it as "CSV UTF-8".
- Rows may end in CRLF, LF or CR.
This items.csv makes three labels:
"1行目
2行目
3行目"
"1行目
2行目"
1行目Tip
A file with one value per line is a CSV file of one column already. Quote only the values that hold a line break, a comma or a double quote. Spreadsheets quote such values when they save a CSV file.
text, pages and pour all take these.
In pages and pour they apply to every label.
| Option | Default | Meaning |
|---|---|---|
-o, --output |
(required) | The .tm2 file to write. Its name, without .tm2, names the label in the app |
-f, --force |
off | Replace the file when it exists |
--tape |
12 |
Tape width in mm: 4, 6, 9, 12, 18, 24, 36 or 50 |
--font-size |
fit | Font size in points. By default the text fills the tape |
--bold |
off | Bold text |
--align |
left |
Where each line goes across: left, center or right |
--valign |
middle |
Where the text goes down: top, middle or bottom |
--rotate |
0 |
Turn the text clockwise: 0, 90, 180 or 270 degrees |
--vertical |
off | Write the text top to bottom |
--fill |
none |
The box behind the text: none, inverted (white on black) or shaded (a light dotted box).[BUG WORKAROUND] With inverted, each line gets its own box. The app inverts only the last line of a box |
--border |
none |
The line around the label: none, box, rounded, top-bottom, top or bottom |
--border-width |
0.7 |
The width of the border. 0.35 is thin, 2.8 is thick |
--icon |
none | A PNG, JPEG or GIF file to put before the text, as high as the tape prints |
--background |
none | A PNG, JPEG or GIF file to stretch under the whole label, such as a frame |
--image-mode |
threshold |
How pictures turn black and white: threshold, diffusion or screen |
--image-threshold |
128 |
The grey level, 0 to 255, below which a pixel is black |
--padding |
0 |
Blank between the label's edges and its text, in mm. 1.5 with a border |
--margin |
1 |
Blank tape before and after the label, in mm, 1 to 500 |
--length |
fit | Make the label this long, margins included, in mm, 10 to 3000 |
Only pour takes these:
| Option | Default | Meaning |
|---|---|---|
--code |
text |
Show each value as a code: text, qr, code39, code128 or jan13 |
--column |
Label |
Title of the pour column in the app |
Note
--code jan13 takes 12 digits. The app adds the check digit.
tm2 inspect shows what a .tm2 file holds.
It reads files the app wrote, too.
$ tm2 inspect items.tm2
file: items.tm2 (version 2.0.0, saved 2026/10/18 10:15)
tape: 12 mm, pour, label 12.0 mm long, margin 1.0 mm
drawers: /documents/0/drawers
0 pour x 1.6 y 1.1 w 6.7 h 10.2 mm column 0, size 7.5, OTBGoStdR-Normal, keeps its size
rows: 3, column "Label"
1 1行目 / 2行目 / 3行目
2 1行目 / 2行目
3 1行目
A file of pages shows each page. Pages count from 1, as in the app:
$ tm2 inspect pages.tm2
file: pages.tm2 (version 2.0.0, saved 2026/10/18 10:15)
page 1:
tape: 12 mm, plain, label 12.0 mm long, margin 1.0 mm
drawers: /documents/0/drawers
0 text x 1.6 y 1.1 w 6.7 h 10.2 mm size 7.5, OTBGoStdR-Normal, "1行目\n2行目\n3行目"
page 2:
tape: 12 mm, plain, label 12.2 mm long, margin 1.0 mm
drawers: /documents/1/drawers
0 text x 0.0 y 1.1 w 10.2 h 10.2 mm size 11.5, OTBGoStdR-Normal, "1行目\n2行目"
page 3:
tape: 12 mm, plain, label 22.5 mm long, margin 1.0 mm
drawers: /documents/2/drawers
0 text x 0.0 y 1.1 w 20.5 h 10.2 mm size 23, OTBGoStdR-Normal, "1行目"
- Each drawer starts with its index.
drawers:gives the path of the list, for a patch. - After the size and the font come the settings that differ from a new drawer, such as
align centerorborder box 0.7. --jsonprints every key of the file, at the paths a patch takes. A picture shows as its size.
tm2 edit changes a .tm2 file by a patch.
It reads the patch from a file, or from stdin without one.
Without -o, it changes the file in place.
With -o, the label takes the name of the new file in the app, as a label made there would.
A patch that sets /fileName keeps its own name.
Warning
An edit in place writes over the file, as --force does.
A failed write, such as on a full disk, can leave it broken.
Keep a copy of a file the app saved, or write to another with -o.
This patch.json changes the third row, and centres the text:
[
{"op": "replace", "path": "/tableData/2/0", "value": "4行目"},
{"op": "replace", "path": "/documents/0/drawers/0/TIAlignment", "value": 1}
]tm2 edit items.tm2 patch.jsonwrote items.tm2: 3 labels, 12.0 mm each, on 12 mm tape
A patch is a JSON Patch (RFC 6902).
It applies to the file as tm2 inspect --json prints it.
tm2 edit, edit_label and File.Patch take it.
| Op | What it does |
|---|---|
replace |
Sets the value of a key, or of an item of an array |
add |
Puts an item in an array. - as the index puts it last. On a key, it is replace |
remove |
Takes an item out of an array |
move, copy |
Moves or copies a value from from to path |
test |
Fails the patch unless the value at path is value |
What a patch keeps:
| Rule | Why |
|---|---|
| A key is not added or removed | The app writes every key. A new key would be one no file of the app has |
| A whole file, document or drawer put in place needs every key it has, and no other | A key left out would be written as 0, not as the app sets it. So type changes only with the keys of the new type |
A drawer added starts as the app makes one of its type: text, pour or image |
It needs only type and the keys to change. Its frame is the label's frame |
| A drawer of a type tm2 does not know may move, but not be copied or changed | It is written as it was read, so its identifier cannot change |
The empty key after a drawer's identifier stays |
The app writes it |
| A copy of a document or drawer, or one with no identifier, gets a new identifier | The app gives each one its own |
hasPlaceholder is cleared once TIText has text |
The app shows the placeholder instead of the text while it is set |
With two or more pages, each gets MultiplePage |
The app lists the pages only then |
A row of tableData gets empty cells up to 50 |
The app writes 50 columns |
| A picture is checked, and a JPEG or GIF is made PNG | The app stores pictures as PNG |
| A value the app crashes on fails the patch | Such a file is never written |
Either every operation applies, or nothing changes.
Important
A patch does not lay the label out again. Text made longer keeps its frame, and the app fits it there.
Set frame too when the size must change. A frame is x, y, width and height in points. 1 mm is 2.835 points.
Warning
[BUG WORKAROUND]
Inverted text (TIFillMode 1) can have only one line in a text box.
With two or more lines, the app prints only the last line in white. The other lines print in black. The app's preview does not show this.
Put each line in its own text box, one under the other, with no gap.
tm2 text --fill inverted and the other commands already do this.
image_path in an operation gives a picture, from a PNG, JPEG or GIF file.
It is the value of a path that ends in /image, given with no value.
Or it is the image of a drawer of type image, given as the value:
[{"op": "add", "path": "/documents/0/drawers/-", "value": {"type": "image", "frame": [0, 0, 20, 20]}, "image_path": "/path/to/logo.png"}]The numbers of the keys that show in the app:
| Key | Values |
|---|---|
TIAlignment |
0 left, 1 center, 2 right |
TIVerticalAlignment |
0 top, 1 middle, 2 bottom |
TIFillMode |
0 none, 1 inverted, 2 shaded |
Rotate |
0 none, 1 180°, 2 270°, 3 90° |
BIType |
0 none, 1 box, 2 rounded, 3 top-bottom, 4 top, 5 bottom |
Attribute (pour) |
0 text, 1 QR, 2 CODE39, 3 CODE128, 4 JAN-13 |
imageMode |
1 diffusion, 2 threshold, 3 screen |
TIFontName |
OTBGoStdR-Normal regular, OTBGoStdB-Normal bold |
| Function | Returns |
|---|---|
NewText(tape, text, opts...) |
A file of one label |
NewPages(tape, texts, opts...) |
A file of a page per string, each fitted |
NewPour(tape, column, values, opts...) |
A pour file of a label per string, under the column's title |
ReadFile(path), ParseFile(r) |
A file read back |
A File writes itself with WriteFile or WriteTo.
Patch changes it by a patch.
f, err := tm2.ReadFile("items.tm2")
if err != nil {
return err
}
err = f.Patch(tm2.PatchOp{
Op: "replace",
Path: "/tableData/2/0",
Value: jsontext.Value(`"4行目"`),
})
if err != nil {
return err
}
return f.WriteFile("items.tm2")Each With function sets one thing. What no option sets is as the app sets it.
| Option | Default | Meaning |
|---|---|---|
WithName(name) |
"label" |
Names the label in the app, without .tm2 |
WithTime(t) |
now | The file's date |
WithFontSize(pt) |
fit | Font size in points. By default the text fills the tape |
WithBold() |
off | Bold text |
WithAlign(a) |
AlignLeft |
Where each line goes across: AlignLeft, AlignCenter or AlignRight |
WithVerticalAlign(a) |
VerticalAlignMiddle |
Where the text goes down: VerticalAlignTop, VerticalAlignMiddle or VerticalAlignBottom |
WithRotation(r) |
RotationNone |
Turn the text clockwise: RotationNone, Rotation90, Rotation180 or Rotation270 |
WithVertical() |
off | Write the text top to bottom |
WithFill(m) |
FillNone |
The box behind the text: FillNone, FillInverted or FillShaded |
WithBorder(b) |
BorderNone |
The line around the label: BorderNone, BorderBox, BorderRounded, BorderTopBottom, BorderTop or BorderBottom |
WithBorderWidth(w) |
0.7 |
The width of the border |
WithIcon(picture) |
none | PNG, JPEG or GIF bytes to put before the text |
WithBackground(picture) |
none | PNG, JPEG or GIF bytes to stretch under the whole label |
WithImageMode(m) |
ImageModeThreshold |
How pictures turn black and white: ImageModeThreshold, ImageModeDiffusion or ImageModeScreen |
WithImageThreshold(level) |
128 |
The grey level, 0 to 255, below which a pixel is black |
WithPaddingMM(mm) |
0 |
Blank between the label's edges and its text. 1.5 with a border |
WithMarginMM(mm) |
1 |
Blank tape before and after the label, 1 to 500 |
WithLengthMM(mm) |
fit | Make the label this long, margins included, 10 to 3000 |
WithCode(k) |
CodeText |
Pour only. Show each value as a code: CodeText, CodeQR, CodeCODE39, CodeCODE128 or CodeJAN13 |
The type of an option says which functions take it:
| Type | Returned by | Taken by |
|---|---|---|
Option |
Every With function but WithCode |
NewText, NewPages and NewPour |
PourOnlyOption |
WithCode |
NewPour |
- So
WithCodegiven toNewTextdoes not compile. - To build a list of options, make it a
[]tm2.TextOptionor a[]tm2.PourOption.
f, err := tm2.NewPour(
tm2.Tape12mm,
"URL",
[]string{"https://example.com/"},
tm2.WithCode(tm2.CodeQR),
)Every error wraps one of these. Test it with errors.Is.
Errors of the file system come back as they are, such as fs.ErrNotExist.
| Error | When |
|---|---|
ErrInvalidOptions |
An option out of range, such as a margin under 1 mm |
ErrCrashesApp |
A value the app crashes on, such as alignment 3. It is never written |
ErrTapeNotSupported |
A tape width whose sizes are not known |
ErrUnknownName |
A name ParseRotation, ParseCodeKind and the like do not know |
ErrNoText |
No text, no values or pages, or one of blanks only |
ErrBadText |
Text that is not UTF-8 |
ErrBadFile |
A file that is not a .tm2 file the package can read, or one it cannot encode |
ErrOverLimit |
More rows, characters or columns than the app takes, or a label over 3000 mm |
ErrNoRoom |
No room on the tape. Padding takes it, or there are too many lines, a line too long to run across, a font size too large, or a fixed length too short |
ErrBadImage |
A picture that is not PNG, JPEG or GIF, or over 8192 by 8192 pixels |
ErrBadCode |
A value the kind of code cannot hold, such as 13 digits for JAN-13 |
ErrBadPatch |
An operation Patch cannot apply, such as one that adds a key, or a patch that makes no .tm2 file |
ReadFileandParseFileread a.tm2file, from the app or from this package.- Drawer types the package does not know are kept as
RawDrawer. - Keys it does not know, of the file, a document or a drawer, are kept in their
Unknownfield. - Writing the file back keeps both as they were.
tepra.tm2 is under the MIT License.
The mascot is a Go gopher holding a strip of label tape. The Go gopher was designed by Renée French, under CC BY 4.0.